diff --git a/4.4.0/404.html b/4.4.0/404.html new file mode 100644 index 0000000..db1fd17 --- /dev/null +++ b/4.4.0/404.html @@ -0,0 +1,1973 @@ + + + + + + + + + + + + + + + + + + + + + + + RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/API-and-Development/API-Reference/index.html b/4.4.0/API-and-Development/API-Reference/index.html new file mode 100644 index 0000000..9f8b0ba --- /dev/null +++ b/4.4.0/API-and-Development/API-Reference/index.html @@ -0,0 +1,2567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + API Reference - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

API Reference

+

RomM provides a comprehensive REST API that allows you to programmatically interact with your RomM instance. Most API endpoints are authenticated and follow RESTful conventions.

+

Interactive Documentation

+

RomM automatically generates interactive API documentation using OpenAPI (Swagger). You can access the interactive API docs directly from your running instance:

+
    +
  • Swagger UI: Available at http://your-instance:3000/api/docs
  • +
  • ReDoc: Available at http://your-instance:3000/api/redoc
  • +
+

These interactive docs allow you to:

+
    +
  • Browse all available endpoints
  • +
  • View request/response schemas
  • +
  • Test API calls directly from your browser
  • +
  • Understand authentication requirements
  • +
  • Download the OpenAPI specification
  • +
+

Base URL

+

The API base URL is typically:

+
http://your-instance:3000/api
+
+

Replace your-instance with your actual RomM instance URL or IP address.

+

Authentication

+

All API endpoints require authentication. RomM supports:

+
    +
  • Basic HTTP Authentication - Username and password
  • +
  • OAuth2 Password Bearer - Token-based authentication (recommended for API usage)
  • +
+

When using OAuth2, you'll need to obtain a token from /api/token endpoint and include it in the Authorization header as Bearer <token>.

+

OAuth2 Scopes

+

The API uses OAuth2 scopes to control access to different resources:

+

Read Scopes:

+
    +
  • me.read - View your profile
  • +
  • roms.read - View ROMs
  • +
  • platforms.read - View platforms
  • +
  • assets.read - View assets
  • +
  • firmware.read - View firmware
  • +
  • roms.user.read - View user-rom properties
  • +
  • collections.read - View collections
  • +
  • users.read - View users
  • +
+

Write Scopes:

+
    +
  • me.write - Modify your profile
  • +
  • assets.write - Modify assets
  • +
  • roms.user.write - Modify user-rom properties
  • +
  • collections.write - Modify collections
  • +
  • roms.write - Modify ROMs
  • +
  • platforms.write - Modify platforms
  • +
  • firmware.write - Modify firmware
  • +
  • users.write - Modify users
  • +
  • tasks.run - Run tasks
  • +
+

API Endpoints Overview

+

The RomM API provides comprehensive endpoints for managing all aspects of your ROM collection:

+

Core Resources

+
    +
  • Platforms - Manage and configure gaming platforms
  • +
  • ROMs - Full CRUD operations for ROM files with extensive filtering, searching, and metadata matching
  • +
  • Collections - Create and manage ROM collections, smart collections, and virtual collections
  • +
  • Users - User management, authentication, invite links, and profiles
  • +
+

Supporting Features

+
    +
  • Authentication - OAuth2 token management, OIDC login, password resets
  • +
  • Search - Metadata provider search for ROMs and covers
  • +
  • Tasks - Background task management and execution
  • +
  • Assets - Save files, states, screenshots management
  • +
  • Firmware - Upload and manage firmware files for emulation
  • +
  • Configuration - System configuration, platform bindings, and exclusions
  • +
  • Feeds - Integration feeds for WebRcade and Tinfoil
  • +
  • Statistics - System statistics and resource tracking
  • +
+

For complete endpoint documentation including request/response schemas, query parameters, and authentication requirements, visit the interactive API documentation at /api/docs or /api/redoc on your RomM instance.

+

Example Usage

+

Using cURL

+
# Get all libraries
+curl -u username:password http://your-instance:3000/api/libraries
+
+# Get a specific ROM
+curl -u username:password http://your-instance:3000/api/roms/123
+
+# Create a new ROM entry
+curl -X POST -u username:password \
+  -H "Content-Type: application/json" \
+  -d '{"name": "New ROM", "platform_id": 1}' \
+  http://your-instance:3000/api/roms
+
+

Using Python

+
import requests
+from requests.auth import HTTPBasicAuth
+
+# Setup authentication
+auth = HTTPBasicAuth('username', 'password')
+base_url = 'http://your-instance:3000/api'
+
+# Get all libraries
+response = requests.get(f'{base_url}/libraries', auth=auth)
+libraries = response.json()
+
+# Get a specific ROM
+response = requests.get(f'{base_url}/roms/123', auth=auth)
+rom = response.json()
+
+

Using JavaScript/Node.js

+
const axios = require("axios");
+
+// Setup authentication
+const api = axios.create({
+    baseURL: "http://your-instance:3000/api",
+    auth: {
+        username: "username",
+        password: "password",
+    },
+});
+
+// Get all libraries
+const libraries = await api.get("/libraries");
+
+// Get a specific ROM
+const rom = await api.get("/roms/123");
+
+

OpenAPI Specification

+

You can download the complete OpenAPI specification from your RomM instance:

+
http://your-instance:3000/api/openapi.json
+
+

This specification can be imported into API testing tools like Postman, used to generate client libraries, or used for API mocking.

+

Getting Help

+

For API-specific questions or issues:

+
    +
  1. Check the interactive documentation at /api/docs or /api/redoc on your instance
  2. +
  3. Review the code in the RomM repository
  4. +
  5. Open an issue on GitHub
  6. +
  7. Join the Discord community
  8. +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/API-and-Development/Contributing/index.html b/4.4.0/API-and-Development/Contributing/index.html new file mode 100644 index 0000000..04dbdf7 --- /dev/null +++ b/4.4.0/API-and-Development/Contributing/index.html @@ -0,0 +1,2385 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Contributing - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Contributing to RomM

+

Thank you for considering contributing to RomM! This document outlines some guidelines to help you get started with your contributions.

+

If you're looking to implement a large feature or make significant changes to the project, it's best to open an issue first AND join the Discord to discuss your ideas with the maintainers.

+

Code of Conduct

+

Please note that this project adheres to the Contributor Covenant code of conduct. By participating in this project, you are expected to uphold this code.

+

AI Assistance Notice

+
+

[!IMPORTANT]

+

If you are using any kind of AI assistance to contribute to RomM, it must be disclosed in the pull request.

+
+

If you are using any kind of AI assistance while contributing to RomM this must be disclosed in the pull request, along with the extent to which AI assistance was used (e.g. docs only vs. code generation). If PR responses are being generated by an AI, disclose that as well. As a small exception, trivial tab-completion doesn't need to be disclosed.

+

An example disclosure:

+
+

This PR was written primarily by Claude Code.

+
+

Or a more detailed disclosure:

+
+

I consulted ChatGPT to understand the codebase but the solution +was fully authored manually by myself.

+
+

Failure to disclose this is rude to the human operators on the other end of the pull request, but it also makes it difficult to determine how much scrutiny to apply to the contribution.

+

In a perfect world, AI assistance would produce equal or higher quality work than any human. That isn't the world we live in today, and in most cases it's generating slop.

+

Please be respectful to maintainers and disclose AI assistance.

+

Contributing to the Docs

+

If you would like to contribute to the project's documentation, open a pull request against the docs repo. We welcome any contributions that help improve the documentation (new pages, updates, or corrections).

+

Adding Translations

+

If you would like to translate the project into another language, create a new folder under the frontend/src/locales directory, and follow the existing language files as a template. Once you've created the new language file, open a pull request to add it to the project.

+

How to Contribute Code

+
    +
  1. Fork the repository.
  2. +
  3. Clone your forked repository: git clone https://github.com/your-username/romm.git
  4. +
  5. Checkout the master branch: git checkout master
  6. +
  7. Follow the steps in the developer setup guide
  8. +
  9. Create a new branch for your feature/fix: git checkout -b feature-or-fix-name
  10. +
  11. Make your changes and commit them with descriptive commit messages: git commit -am 'Add feature XYZ'
  12. +
  13. Push your changes to your fork: git push origin feature-or-fix-name
  14. +
  15. Open a pull request to the master branch of the original repository.
  16. +
+

Pull Request Guidelines

+
    +
  • Make sure your code follows the project's coding standards.
  • +
  • Test your changes locally before opening a pull request.
  • +
  • Update the documentation if necessary.
  • +
  • Ensure all existing tests pass, and add new tests for new functionality.
  • +
  • Use clear and descriptive titles and descriptions for your pull requests.
  • +
+

Code Style

+

Follow the existing code style used throughout the project. If working with VSCode or a similar editor, consider installing these extensions:

+ +

Issue Reporting

+

If you encounter any bugs or have suggestions for improvements, please create an issue on GitHub. Provide as much detail as possible, including steps to reproduce the issue if applicable.

+

Licensing

+

By contributing to RomM, you agree that your contributions will be licensed under the project's LICENSE.

+
+

Thank you for contributing to RomM! Your help is greatly appreciated.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/API-and-Development/Development-Setup/index.html b/4.4.0/API-and-Development/Development-Setup/index.html new file mode 100644 index 0000000..014b903 --- /dev/null +++ b/4.4.0/API-and-Development/Development-Setup/index.html @@ -0,0 +1,2288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Development Setup - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Setting up RomM for development

+

Docker provides a quick and easy way to get started with RomM by encapsulating all dependencies within Docker containers. This guide will walk you through the process of setting up RomM for development using Docker.

+

Environment setup

+

Create the mock structure with at least one rom and empty config for manual testing

+
mkdir -p romm_mock/library/roms/switch
+touch romm_mock/library/roms/switch/metroid.xci
+mkdir -p romm_mock/resources
+mkdir -p romm_mock/assets
+mkdir -p romm_mock/config
+touch romm_mock/config/config.yml
+
+

Copy env.template to .env and fill the variables

+
cp env.template .env
+
+
ROMM_BASE_PATH=/app/romm
+DEV_MODE=true
+
+

Build the image

+
docker compose build  # or `docker compose build --no-cache` to rebuild from scratch
+
+

Spin up the Docker containers

+
docker compose up -d
+
+

And you're done! You can access the app at http://localhost:3000. Any changes made to the code will be automatically reflected in the app thanks to the volume mounts.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/API-and-Development/index.html b/4.4.0/API-and-Development/index.html new file mode 100644 index 0000000..73a227e --- /dev/null +++ b/4.4.0/API-and-Development/index.html @@ -0,0 +1,2094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + API & Development - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

API & Development

+

Welcome to the RomM API & Development documentation. This section contains resources for developers looking to interact with RomM programmatically or contribute to its development.

+

Contents

+
    +
  • API Reference - Complete API documentation with endpoints, schemas, and examples
  • +
  • Contributing - Guidelines for contributing code, translations, and documentation
  • +
  • Development Setup - How to set up your development environment
  • +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Getting-Started/Authentication/index.html b/4.4.0/Getting-Started/Authentication/index.html new file mode 100644 index 0000000..f108588 --- /dev/null +++ b/4.4.0/Getting-Started/Authentication/index.html @@ -0,0 +1,2417 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Authentication - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Authentication

+ +

RomM provides support for various forms of authentication, granting flexibility in securing access to its features.

+

Setup

+

You'll want to set the following environment variable before starting RomM:

+
    +
  • ROMM_AUTH_SECRET_KEY is required and can be generated with openssl rand -hex 32
  • +
+

Sessions

+

When the /login endpoint is called with valid credentials, a session_id is generated, stored as a cookie and sent to the browser. The same token is used to create a cache entry in Valkey (or in-memory if Valkey is disabled) which maps the token to the user. This way no sensitive information is stored on the client.

+

Roles

+

A user can have one of the following roles:

+
    +
  • VIEWER: Can view platforms and ROMs, download ROMs, and edit own profile
  • +
  • EDITOR: Can create/edit/delete platforms and ROMs
  • +
  • ADMIN: Can view all users, and create/edit/disable/delete users
  • +
+

As permissions are additive, editors will have all permissions of the viewer role, and admins all those of the editor role.

+

Basic Authentication

+

Requests can be made to protected API endpoints with an authorization header. The token is the base64 encoded value of username:password.

+

Example using cURL:

+
curl https://romm.local/api/platforms -H 'Authorization: Basic YWRtaW46aHVudGVyMg=='
+
+

OAuth

+

Along with the above forms of authentication, we've added an endpoint to generate expiring, scope-limited authentication tokens (/api/token). Successfully authenticating with that endpoint with return an access_token valid for 15 minutes, and a refresh_token valid for 2 weeks. The refresh_token can be used to generate a new access_token when needed.

+

The /api/token endpoint requires a username, password, and a list of scopes in the format read:roms write:roms read:platforms .... The list of scopes and endpoints are available to browse via Swagger UI or ReDoc (see next section).

+

Note: As of now, only the legacy password grant type is supported. We plan to eventually add support for Client Credentials.

+

OpenAPI

+

The API endpoints are fully documented and compliant with the OpenAPI specification. Explore the API endpoints using the Swagger UI interface at /api/docs and the ReDoc interface at /api/redoc, or view the raw JSON at /openapi.json.

+

For more information on OpenAPI, visit the OpenAPI Specification website.

+

FAQ

+

Can I disable authentication?

+

No, authentication is required and enabled by default.

+

I want to allow an EDITOR to edit ROMs but not delete them. Can I do that?

+

At this time, fine-grain control over permissions within a role is not supported. This decision was taking in order to simplify user management in the client, and authentication/permission code on the server.

+

Is authentication safe/robust? Can I trust it?

+

We've done our best to build an authentication system that is simple, clear and comprehensible. We have automated tests which verify that access is granted when it should be, and blocked when not (invalid credentials, missing permissions, expired access tokens, etc.). That being said, we welcome any reviews of our authentication and permission flows, PRs to fix issues, and new tests to cover edge cases.

+

I found an bug/issue with authentication. How do I report it?

+

Please report bugs in our authentication/permission system privately by submitting a vulnerability report.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Getting-Started/Configuration-File/index.html b/4.4.0/Getting-Started/Configuration-File/index.html new file mode 100644 index 0000000..a4742e4 --- /dev/null +++ b/4.4.0/Getting-Started/Configuration-File/index.html @@ -0,0 +1,2814 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Configuration File - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Configuration File

+ +

Below is a breakdown of each section of the config.yml file and its purpose. You can find a full example of the file in the config.example.yml file.

+
+

Exclude Section

+

Control which platforms, ROMs, or files to ignore during scanning.

+

Platforms

+

Exclude entire platforms (folders) from being scanned.

+

Example:

+
exclude:
+    platforms: ["ps", "ngc", "gba"]
+
+

ROMs

+

Fine-tune which ROMs or files are excluded.

+

Single File ROMs

+

Applies to ROMs that are single files (not in subfolders).

+
    +
  • extensions: Exclude files by extension
  • +
  • names: Exclude files by name or pattern (supports Unix wildcards)
  • +
+

Example:

+
exclude:
+    roms:
+        single_file:
+            extensions: ["xml", "txt"]
+            names: ["info.txt", "._*", "*.nfo"]
+
+

Multi-File ROMs

+

Applies to ROMs stored as folders (multi-disc, with DLC, etc.).

+
    +
  • names: Exclude entire folders by name
  • +
  • parts.names: Exclude files by name or pattern from within multi-file ROM folders
  • +
  • parts.extensions: Exclude files by extension from within multi-file ROM folders
  • +
+

Example:

+
exclude:
+    roms:
+        multi_file:
+            names: ["final fantasy VII", "DLC"]
+            parts:
+                names: ["data.xml", "._*"]
+                extensions: ["xml", "txt"]
+
+
+

System Section

+

Customize how RomM interprets your folder and platform names.

+

Custom Folder Names

+

Map your custom folder names to RomM's recognized platform names.

+

Example:

+
system:
+    platforms:
+        gc: "ngc" # Treats 'gc' folder as GameCube
+        psx: "ps" # Treats 'psx' folder as PlayStation
+
+

Versions

+

Associate a platform with its main version. This also tells RomM to fetch metadata from the main version source.

+

Example:

+
system:
+    versions:
+        naomi: "arcade"
+
+
+

Filesystem Section

+

Specify the folder name where your ROMs are located if it differs from the default.

+

Example:

+

If your ROMs folder is named my_roms instead of roms:

+
filesystem:
+    roms_folder: "my_roms"
+
+
+

Scan Section

+

Configure metadata scanning priorities and media assets to download.

+

Priority

+

Customize the order in which metadata providers are queried during scans.

+

Metadata

+

Controls metadata provider priority order.

+

Provider list in default order:

+
    +
  • igdb - IGDB (highest priority)
  • +
  • moby - MobyGames
  • +
  • ss - Screenscraper
  • +
  • ra - RetroAchievements
  • +
  • launchbox - Launchbox
  • +
  • gamelist - ES-DE gamelist.xml
  • +
  • hasheous - Hasheous
  • +
  • flashpoint - Flashpoint Project
  • +
  • hltb - HowLongToBeat (lowest priority)
  • +
+

Example:

+
scan:
+    priority:
+        metadata:
+            - "igdb"
+            - "ss"
+            - "moby"
+
+

Artwork

+

Controls artwork provider priority order for cover art and screenshots.

+

Default: Same as priority.metadata

+

Example:

+
scan:
+    priority:
+        artwork:
+            - "igdb"
+            - "ss"
+            - "moby"
+
+

Region

+

Sets preferred region for cover art and game title (Screenscraper only).

+

Default: ["us", "wor", "ss", "eu", "jp"]

+

Example:

+
scan:
+    priority:
+        region:
+            - "us"
+            - "eu"
+            - "jp"
+
+

Language

+

Sets preferred language for cover art and game title (Screenscraper only).

+

Default: ["en", "fr"]

+

Example:

+
scan:
+    priority:
+        language:
+            - "en"
+            - "es"
+            - "fr"
+
+

Media

+

Configures which media assets to download (Screenscraper and ES-DE gamelist.xml only).

+

Media types:

+
    +
  • box2d - Normal cover art (always enabled)
  • +
  • box3d - 3D box art
  • +
  • miximage - Mixed image of multiple media
  • +
  • physical - Disc, cartridge, etc.
  • +
  • screenshot - Screenshot (enabled by default)
  • +
  • title_screen - Title screen
  • +
  • marquee - Transparent logo
  • +
  • fanart - User uploaded artwork
  • +
  • bezel - Bezel displayed around the EmulatorJS window
  • +
  • manual - Manual in PDF format (enabled by default)
  • +
  • video - Gameplay video (warning: large file size)
  • +
+

Example:

+
scan:
+    media:
+        - box2d
+        - screenshot
+        - manual
+        - bezel
+
+
+

EmulatorJS Section

+

Configure EmulatorJS per-core options and controls.

+

Debug

+

Enable debug mode to log available options to the browser console.

+

Example:

+
emulatorjs:
+    debug: true
+
+

Cache Limit

+

Cache limit per ROM in bytes. Set to null for unlimited.

+

Example:

+
emulatorjs:
+    cache_limit: 52428800 # 50 MB
+
+

Settings

+

Configure core-specific settings. Use default to apply settings to all cores.

+

Example:

+
emulatorjs:
+    settings:
+        parallel_n64: # Use the exact core name
+            vsync: disable
+        snes9x:
+            snes9x_region: ntsc
+        default: # These settings apply to all cores
+            fps: show
+
+

Controls

+

Map keyboard and controller controls for each player.

+

Example (2-player SNES):

+
emulatorjs:
+    controls:
+        snes9x:
+            0: # Player 1
+                0: # Button mapping
+                    value: x # Keyboard mapping
+                    value2: BUTTON_2 # Controller mapping
+            1: # Player 2
+                0:
+                    value: /
+                    value2: BUTTON_2
+
+

See EmulatorJS documentation for control mapping details.

+
+ +
+

Tip

+

You can find examples of full binded batocera or es-de config files.

+
+ +
+

Warning

+

Only uncomment or add the lines you need. Any omitted or empty sections will use RomM's defaults.

+
+

For a full example, see the config.example.yml file.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Getting-Started/Environment-Variables/index.html b/4.4.0/Getting-Started/Environment-Variables/index.html new file mode 100644 index 0000000..0ce44a6 --- /dev/null +++ b/4.4.0/Getting-Started/Environment-Variables/index.html @@ -0,0 +1,2818 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Environment Variables - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Environment Variables

+ +

This is a complete list of available environment variables; required variables are marked with a ✓.

+ +
+

Tip

+

You can also set environment variables with a _FILE suffix, which will load the contents of the file specified in the variable into the variable without the suffix. For example, setting ROMM_AUTH_SECRET_KEY_FILE=/run/secrets/romm_auth_secret_key and creating a file with the secret key at the specified path will set ROMM_AUTH_SECRET_KEY to the contents of the file. Learn more.

+
+

Application settings

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDescriptionRequiredDefault
ROMM_AUTH_SECRET_KEYGenerate a key with openssl rand -hex 32✓
DISABLE_CSRF_PROTECTIONDisables CSRF protection (not recommended)false
DISABLE_DOWNLOAD_ENDPOINT_AUTHDisable auth on download endpoint (WebRcade, Tinfoil)false
DISABLE_USERPASS_LOGINDisables login with username and password (when using OIDC)false
SESSION_MAX_AGE_SECONDSMaximum age of a session (in seconds)1209600 (14 days)
KIOSK_MODERead-only mode for public displays or kiosksfalse
UPLOAD_TIMEOUTTimeout for file uploads (in seconds)600 (10 minutes)
SCAN_TIMEOUTTimeout for the background scan/rescan tasks (in seconds)14400 (4 hours)
SCAN_WORKERSNumber of worker processes for scanning tasks1
TASK_TIMEOUTTimeout for the the rest of the background tasks (in seconds)300 (5 minutes)
TASK_RESULT_TTLHow long to keep task results in Valkey (in seconds)86400 (24 hours)
SEVEN_ZIP_TIMEOUTTimeout for 7-Zip operations (in seconds)60
DISABLE_EMULATOR_JSDisables playing in browser with EmulatorJSfalse
DISABLE_RUFFLE_RSDisables playing flash games with RuffleRSfalse
YOUTUBE_BASE_URLBase URL for alternate frontends (Piped, Invidious, etc.)https://www.youtube.com
TZSets the timezoneUTC
ROMM_PORTPort on which the application listens8080
ROMM_BASE_PATHBase folder path for library, resources and assets/romm
ROMM_BASE_URLBase url for properly display container logs links (like register or recover password)0.0.0.0
LOGLEVELLogging level for the appINFO
FORCE_COLORForces color outputfalse
NO_COLORDisables color outputfalse
WEB_SERVER_CONCURRENCYNumber of processes running the app1
WEB_SERVER_KEEPALIVEWaiting time for requests on a Keep-Alive connection (in seconds)2
WEB_SERVER_MAX_REQUESTSMaximum number of requests a worker will process before restarting1000
WEB_SERVER_MAX_REQUESTS_JITTERRandom jitter to add to the maximum number of requests a worker will process before restarting100
WEB_SERVER_TIMEOUTTimeout for web server requests (in seconds)300
WEB_SERVER_WORKER_CONNECTIONSMaximum number of simultaneous clients a single process can handle1000
+

Dependencies

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDescriptionRequiredDefault
DB_HOSTHost name of database instance✓127.0.0.1
DB_PORTPort number of database instance3306
DB_NAMEShould match MYSQL_DATABASE in MariaDBromm
DB_USERDatabase username (in MariaDB, should match MARIADB_USER)✓
DB_PASSWDDatabase password (in MariaDB, should match MARIADB_PASSWORD)✓
DB_QUERY_JSONExtra query parameters for the database connection, in JSON format (e.g. {"unix_socket": "/path/to/db"})
ROMM_DB_DRIVERDatabase driver to use (options: mariadb, mysql, postgresql)mariadb
REDIS_HOSTHost name of Redis/Valkey instance127.0.0.1
REDIS_PORTPort number of Redis/Valkey instance6379
REDIS_USERNAMEUsername for Redis/Valkey instance
REDIS_PASSWORDPassword for Redis/Valkey instance
REDIS_DBDatabase number for Redis/Valkey instance0
REDIS_SSLEnable SSL for Redis/Valkey instancefalse
SENTRY_DSNDSN for Sentry error tracking
+

Metadata providers

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDescriptionRequiredDefault
IGDB_CLIENT_IDClient ID for IGDB API
IGDB_CLIENT_SECRETClient secret for IGDB API
SCREENSCRAPER_USERScreenscraper username
SCREENSCRAPER_PASSWORDScreenscraper password
MOBYGAMES_API_KEYMobyGames secret API key
STEAMGRIDDB_API_KEYSteamGridDB secret API key
RETROACHIEVEMENTS_API_KEYRetroachievements secret API key
REFRESH_RETROACHIEVEMENTS_CACHE_DAYSRetroachievements metadata cache refresh (in days)30
LAUNCHBOX_API_ENABLEDEnable LaunchBox API integrationfalse
PLAYMATCH_API_ENABLEDEnable PlayMatch API integrationfalse
HASHEOUS_API_ENABLEDEnable Hasheous API integrationfalse
FLASHPOINT_API_ENABLEDEnable Flashpoint API integrationfalse
HLTB_API_ENABLEDEnable HowLongToBeat API integrationfalse
+

Authentication

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDescriptionRequiredDefault
OIDC_ENABLEDEnable OpenID Connect (OIDC) authenticationfalse
OIDC_PROVIDERName of the OIDC provider in use
OIDC_CLIENT_IDClient ID for OIDC authentication
OIDC_CLIENT_SECRETClient secret for OIDC authentication
OIDC_REDIRECT_URIAbsolute redirect URI for OIDC authentication
OIDC_SERVER_APPLICATION_URLAbsolute URL of the OIDC server application
OIDC_TLS_CACERTFILEPath to a file containing trusted CA certificates
+

Background tasks

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDescriptionRequiredDefault
ENABLE_RESCAN_ON_FILESYSTEM_CHANGEEnable re-scanning of library when filesystem changesfalse
RESCAN_ON_FILESYSTEM_CHANGE_DELAYDelay before re-scanning library when filesystem changes (in minutes)5
ENABLE_SCHEDULED_RESCANEnable scheduled re-scanning of libraryfalse
SCHEDULED_RESCAN_CRONCron expression for scheduled re-scanning"0 3 * * *"
ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDBEnable scheduled updating of Switch TitleDB indexfalse
SCHEDULED_UPDATE_SWITCH_TITLEDB_CRONCron expression for scheduled updating of Switch TitleDB"0 4 * * *"
ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATAEnable scheduled updating of LaunchBox metadatafalse
SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRONCron expression for scheduled updating of LaunchBox metadata"0 4 * * *"
ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBPEnable scheduled conversion of images to WebP formatfalse
SCHEDULED_CONVERT_IMAGES_TO_WEBP_CRONCron expression for scheduled conversion of images to WebP format"0 4 * * *"
ENABLE_SCHEDULED_RETROACHIEVEMENTS_PROGRESS_SYNCEnable scheduled syncing of Retroachievements progressfalse
SCHEDULED_RETROACHIEVEMENTS_PROGRESS_SYNC_CRONCron expression for scheduled syncing of Retroachievements progress"0 4 * * *"
+

Development

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDescriptionRequiredDefault
DEV_MODEEnable development mode (debugging, hot-reloading)false
DEV_HOSTHost for development server127.0.0.1
DEV_PORTPort for development server5000
DEV_SQL_ECHOEnable SQL query logging in development modefalse
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Getting-Started/Folder-Structure/index.html b/4.4.0/Getting-Started/Folder-Structure/index.html new file mode 100644 index 0000000..4f4e47f --- /dev/null +++ b/4.4.0/Getting-Started/Folder-Structure/index.html @@ -0,0 +1,2412 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Folder Structure - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Folder Structure

+ +

RomM requires one of these folder structures for proper operation. It will first attempt to detect Structure A (recommended), and if not found, will fall back to Structure B. This auto-detection ensures flexibility while encouraging organization best practices.

+

Folder Organization

+

RomM organizes content in two main categories: ROMs and BIOS files.

+
    +
  • +

    Structure A (Recommended): Both ROMs and BIOS files have their own dedicated root folders, with platform folders inside each.

    +
      +
    • /roms/{platform}/ - Contains all game files for that platform
    • +
    • /bios/{platform}/ - Contains all BIOS files for that platform
    • +
    +
  • +
  • +

    Structure B (Fallback): Each platform has its own root folder containing both a ROMs folder and a BIOS folder.

    +
      +
    • /{platform}/roms/ - Contains all game files for that platform
    • +
    • /{platform}/bios/ - Contains all BIOS files for that platform
    • +
    +
  • +
+ +
+

Note

+

The BIOS folder is entirely optional and only needed for platforms that require BIOS files.

+
+

When using Docker, the volume mount point differs based on your chosen structure:

+
    +
  • Structure A: Mount the parent folder of the roms folder
  • +
  • Structure B: Mount the parent folder of the platform folders
  • +
+

For multifile games (games stored as folders with multiple files or folders inside), RomM will detect special folders inside the game and will display with special tags in the webUI:

+
    +
  • dlc
  • +
  • hack
  • +
  • manual
  • +
  • mod
  • +
  • patch
  • +
  • update
  • +
  • demo
  • +
  • translation
  • +
  • prototype
  • +
+ +
+

Tip

+

For folder naming conventions, review the Platform Support section. To override default system names in the folder structure (if your directories are named differently), see the Configuration File section.

+
+ + + + + + + + + + + + + +
Structure A (recommended)Structure B (fallback)
+ library/roms/{platform}/{game} + + library/{platform}/roms/{game} +
+
+        library/
+        ├─ roms/
+        │  ├─ gbc/
+        │  │  ├─ game_1.gbc
+        │  │  └─ game_2.gbc
+        │  │
+        │  ├─ gba/
+        │  │  ├─ game_3.gba
+        │  │  └─ game_4/
+        │  │     ├─ game_4.gba
+        │  │     ├─ dlc
+        │  │     │  ├─ game_4_dlc_1.7z
+        │  │     │  └─ game_4_dlc_2.7z
+        │  │     ├─ hack
+        │  │     │  └─ game_4_hardmode.rar
+        │  │     ├─ manual
+        │  │     │  └─ game_4_manual.pdf
+        │  │     ├─ mod
+        │  │     │  └─ game_4_crazy_mode.zip
+        │  │     ├─ patch
+        │  │     │  └─ game_4_patch_v1.1.zip
+        │  │     ├─ update
+        │  │     ├─ demo
+        │  │     ├─ translation
+        │  │     └─ prototype
+        │  │
+        │  └─ ps/
+        │     ├─ game_5/
+        │     │   ├─ game_5_cd_1.iso
+        │     │   └─ game_5_cd_2.iso
+        │     │
+        │     └─ game_6.iso
+        │
+        └─ bios/
+           ├─ gba/
+           │  └─ gba_bios.bin
+           │
+           └─ ps/
+              ├─ scph1001.bin
+              ├─ scph5501.bin
+              └─ scph5502.bin
+      
+
+
+        library/
+        ├─ gbc/
+        │  └─ roms/
+        │     ├─ game_1.gbc
+        │     └─ game_2.gbc
+        │
+        ├─ gba/
+        │  ├─ roms/
+        │  │  ├─ game_3.gba
+        │  │  └─ game_4/
+        │  │     ├─ game_4.gba
+        │  │     ├─ dlc
+        │  │     │  ├─ game_4_dlc_1.7z
+        │  │     │  └─ game_4_dlc_2.7z
+        │  │     ├─ hack
+        │  │     │  └─ game_4_hardmode.rar
+        │  │     ├─ manual
+        │  │     │  └─ game_4_manual.pdf
+        │  │     ├─ mod
+        │  │     │  └─ game_4_crazy_mode.zip
+        │  │     ├─ patch
+        │  │     │  └─ game_4_patch_v1.1.zip
+        │  │     ├─ update
+        │  │     ├─ demo
+        │  │     ├─ translation
+        │  │     └─ prototype
+        │  │
+        │  └─ bios/
+        │     └─ gba_bios.bin
+        │
+        └─ ps/
+           ├─ roms/
+           │  ├─ game_5/
+           │  │  ├─ game_5_cd_1.iso
+           │  │  └─ game_5_cd_2.iso
+           │  │
+           │  └─ game_6.iso
+           │
+           └─ bios/
+              ├─ scph1001.bin
+              ├─ scph5501.bin
+              └─ scph5502.bin
+      
+
+ + +
+

Note

+

RomM can be setup without a prior folder structure. If files are manually uploaded from the webUI, RomM will automatically create the folder Structure A

+
+

Configuration file

+

RomM's behavior can be customized using a config.yml file or through the Library Management page in the Settings menu. You can grab the example config.example.yml file and adapt it to your library.

+

What is shown in the Library Management page is the content of the config.yml. For more details read the configuration file section.

+

Naming Convention

+

Tag Support

+

Games can be tagged with region, revision, or other tags by using parentheses in the file name. Additionally, you can set the region and language by adding a prefix: (USA), [reg-J], (French), [De].

+
    +
  • Revision tags must be prefixed with rev or rev- (e.g. rev v1 or rev-1)
  • +
  • Other tags will also be imported, for example: tetris [1.0001](HACK)[!].gba
  • +
+

Tags can be used to search for games in the search bar. For example, searching for (USA) will return all games with the USA tag.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Getting-Started/Metadata-Providers/index.html b/4.4.0/Getting-Started/Metadata-Providers/index.html new file mode 100644 index 0000000..b97727b --- /dev/null +++ b/4.4.0/Getting-Started/Metadata-Providers/index.html @@ -0,0 +1,2631 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Metadata Providers - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Metadata Providers

+ +

RomM supports multiple metadata providers to enrich your game library with titles, descriptions, cover art, and achievements. You don't need all providers, so this guide covers popular combos and setup instructions.

+ +

Here are some combinations you can use based on your needs:

+

⭐ The French Connection: ScreenScraper + Retroachievements

+
    +
  • Supports 125+ popular systems
  • +
  • ScreenScraper provides titles, descriptions, cover art, screenshots and manuals
      +
    • Also supports hash-based matching (as of v4.4)
    • +
    • With the option for 3D boxes and CD/cartridge covers
    • +
    +
  • +
  • Retroachievements provides achievement progress
  • +
  • Use this if you want to avoid Twitch/Amazon products
  • +
+

ScreenScraper + Retroachievements

+

⭐ The Chef's Choice: Hasheous + IGDB + SteamGridDB + Retroachievements

+
    +
  • Supports 135+. popular systems
  • +
  • Hasheous provides hash-based matching and proxies IGDB data (titles, descriptions and artwork)
  • +
  • IGDB adds additional metadata like related games and screenshots
  • +
  • SteamGridDB provides high-quality alternative cover art
  • +
  • Retroachievements provides achievement progress
  • +
  • This is the recommended setup for most users
  • +
+

Hasheous + IGDB + SteamGridDB + Retroachievements

+

The Twitch Fanboy: IGDB + PlayMatch

+
    +
  • Supports the 200+ systems available on IGDB
  • +
  • Provides titles, descriptions, cover art and related games from IGDB
  • +
  • PlayMatch adds hash-based matching for unmatched files
  • +
  • Use this if you want a single-provider solution
  • +
+

The Quick Starter: Hasheous

+
    +
  • Hash-based matching only ⚠️
  • +
  • Proxies titles, descriptions and cover art from IGDB
  • +
  • Incredibly fast scan times
  • +
  • For users who want to avoid API keys
  • +
+

Setup instructions

+

IGDB

+

IGDB (Internet Game Database) is a popular metadata provider that offers metadata, cover art, screenshots, related games and more.

+

To access the IGDB API you'll need a Twitch account and a valid phone number for 2FA verification. Up-to-date instructions are available in the IGDB API documentation. When registering your application in the Twitch Developer Portal, fill out the form like so:

+
    +
  • Name: Something unique or random like romm-3fca6fd7f94dea4a05d029f654c0c44b or KVV8NDXMSRFJ2MRNPNRSL7GQT
  • +
  • OAuth Redirect URLs: localhost
  • +
  • Category: Application Integration
  • +
  • Client Type: Confidential
  • +
+ +
+

Important

+

The name you pick has to be unique! Picking an existing name will fail silently, with no error messages. We recommend using romm-<random hash>, like romm-3fca6fd7f94dea4a05d029f654c0c44b

+
+

Note the client ID and secret that appear on screen, and use them to set IGDB_CLIENT_ID and IGDB_CLIENT_SECRET in your environment variables.

+ +
+Screenshots +

IGDB Creation +IGDB Secret

+
+

ScreenScraper

+

ScreenScraper.fr is a French provider that offers metadata, cover art, screenshots and manuals, along with the option for 3D boxes and CD/cartridge cover art. It supports a wide range of systems and is a great alternative to IGDB.

+

To access the ScreenScraper API, create a ScreenScraper account and copy the user and password you just created to SCREENSCRAPER_USER and SCREENSCRAPER_PASSWORD respectively.

+

MobyGames

+

MobyGames is a metadata provider that offers metadata, cover art and screenshots.

+

To access the MobyGames API, create a MobyGames account and then visit your profile page. Click the API link under your user name to sign up for an API key. Copy the key shown and use it to set MOBYGAMES_API_KEY.

+ +
+

Important

+

Access to the MobyGames API is a paid feature. While we will continue to support it, we recommend using ScreenScraper instead, as it is free to use.

+
+

LaunchBox

+

The LaunchBox Games Database is a community-driven database that provides metadata, cover art, and screenshots. Like the Launchbox desktop application, RomM downloads the entire database locally and matches games based on their exact filenames.

+

To enable LaunchBox, set LAUNCHBOX_API_ENABLED=true in your environment variables. You can enable regular updates of the database by setting ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA, and set the frequency on the cron job with SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON (defaults to 5:00 AM every day).

+

Hasheous

+

Hasheous is a free, open-source metadata provider that uses file hashes to match games. It proxies IGDB data for titles, descriptions, and cover art, and can provide Retroachievements IDs for matched games.

+

Simply set HASHEOUS_API_ENABLED=true in your environment variables, and future scans will start using the Hasheous API.

+

PlayMatch

+

PlayMatch is a hash-based matching service used in conjunction with IGDB to provide better matching for games, hosted by a member of our community.

+

To enable PlayMatch, set PLAYMATCH_API_ENABLED=true in your environment variables.

+

SteamGridDB

+

SteamGridDB provides custom cover art for games or collections. It's not accessed through the scanner but from the "search cover" button when manually editing a game.

+

To access the SteamGridDB API, you need to login to their website with a Steam account. Once logged in, go to your API tab under the preferences page. Copy the key shown and use it to set STEAMGRIDDB_API_KEY.

+

RetroAchievements

+

RomM is able to display your achievements from RetroAchievements. To sync it with your RomM instance, you need to generate an API key from your RetroAchievements account in your settings.

+

Copy the key shown and use it to set RETROACHIEVEMENTS_API_KEY and perform a UNMATCHED scan targeting the platform you want to match with RetroAchievements.

+

After that, each user needs to set their own username in their profile and sync it with RetroAchievements. A new Achievements tab will appear in the Personal tab in the game details.

+

To avoid unnecessary API calls, a cached file with the RA database is stored in RomM. Refresh time for that cache file can be changed with the environment variable REFRESH_RETROACHIEVEMENTS_CACHE_DAYS.

+ +
+Screenshots +

RA API key +RA details

+
+

Flashpoint

+

The Flashpoint Project Database is a project that enables metadata for 180,000+ flash and browser-based games. Enable this metadata source with the FLASHPOINT_API_ENABLED=true environment variable. If you are adding this provider to an existing RomM setup, perform a UNMATCHED scan with Flashpoint selected to update an existing platform.

+

How Long To Beat

+

The How Long To Beat project provides game completion times for more than 84,000 games. Enable this metadata source with the HLTB_API_ENABLED=true environment variable. If you are adding this provider to an existing RomM setup, perform a UNMATCHED scan with How Long To Beat selected to update an existing platform.

+

Game completion times will be added to a new tab on the details page for supported matched games.

+

ES-DE gamelist.xml

+

EmulationStation, and it's modern successor ES-DE, use a custom XML format to store game metadata. RomM can parse this format and import the assets as cover art and screenshots. You'll need to store the gamelist.xml file and any related assets under the platform folder:

+ +
library/
+└─ roms/
+    └─ gba/
+        ├─ game_1.gba
+        ├─ game_2.gba
+        ├─ gamelist.xml
+        ├─ 3dboxes/
+        │  ├─ game_1.png
+        │  └─ game_2.png
+        ├─ covers/
+        ├─ screenshots/
+        └─ etc...
+
+

Metadata Tags in Filenames

+

Scans will now parse custom metadata tags in the filename that match specific patterns, and use them to fetch game metadata for the specified ID. The supported tags are:

+

(igdb-xxxx) for IGDB +(moby-xxxx) for MobyGames +(ra-xxxx) for RetroAchievements +(ssfr-xxxx) for ScreenScraper +(launchbox-xxxx) for Launchbox +(hltb-xxxx) for HowLongToBeat

+

Filenames will not be renamed by RomM to add tags, as they are a non-standard formatting system and could create conflicts with other software.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Getting-Started/OIDC-Setup/index.html b/4.4.0/Getting-Started/OIDC-Setup/index.html new file mode 100644 index 0000000..f864efd --- /dev/null +++ b/4.4.0/Getting-Started/OIDC-Setup/index.html @@ -0,0 +1,2419 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OIDC Setup - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + + +

OIDC Setup

+

OpenID Connect (OIDC) allows you to authenticate to RomM using external identity providers, enabling Single Sign-On (SSO) and centralized user management. This setup eliminates the need to manage separate credentials for RomM.

+

What is OIDC?

+

OIDC is an identity layer built on top of OAuth2. While OAuth2 primarily handles authorization, OIDC adds authentication, enabling applications to verify a user's identity and obtain profile information. This makes OIDC suitable for SSO solutions, where user identity is central to access management.

+

How It Works

+
    +
  1. Click the OIDC login button on RomM's login page
  2. +
  3. You're redirected to your identity provider
  4. +
  5. Authenticate with your credentials
  6. +
  7. You're redirected back to RomM and logged in automatically
  8. +
+

Supported Identity Providers

+

RomM supports OIDC authentication with the following identity providers:

+

Authelia

+

An open-source authentication and authorization server providing two-factor authentication and SSO. Ideal for self-hosters looking for a lightweight solution.

+

Authentik

+

An open-source identity provider with support for modern authentication protocols, MFA, and comprehensive user management.

+

PocketID

+

A simple OIDC provider that exclusively supports passkey authentication - no passwords required.

+

Zitadel

+

An enterprise-grade, open-source identity and access management platform supporting OAuth2, OIDC, SAML, and passwordless authentication.

+

General Setup Requirements

+

Regardless of which provider you choose, you'll need to configure these environment variables in RomM:

+
OIDC_ENABLED=true
+OIDC_PROVIDER=<provider_name>
+OIDC_CLIENT_ID=<your_client_id>
+OIDC_CLIENT_SECRET=<your_client_secret>
+OIDC_REDIRECT_URI=<your_romm_url>/api/oauth/openid
+OIDC_SERVER_APPLICATION_URL=<your_provider_url>
+
+

Important Notes

+
    +
  • Email matching: Your email address in RomM must match the email in your identity provider
  • +
  • First-time users: Users logging in via OIDC for the first time will be created automatically with viewer permissions
  • +
  • Existing users: Users who already have an account in RomM need to ensure their email addresses match between RomM and the identity provider
  • +
+

Troubleshooting

+

If you encounter issues with OIDC authentication:

+
    +
  • Verify all environment variables are set correctly
  • +
  • Check that the redirect URI matches exactly between RomM and your identity provider
  • +
  • Ensure your email address in RomM matches your email in the identity provider
  • +
  • Review the Authentication Issues documentation
  • +
  • Check your identity provider's logs for any authentication errors
  • +
+

Next Steps

+

Choose a provider from the list above and follow its specific setup guide. Each guide provides detailed step-by-step instructions for configuring both the identity provider and RomM.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Getting-Started/Quick-Start-Guide/index.html b/4.4.0/Getting-Started/Quick-Start-Guide/index.html new file mode 100644 index 0000000..a3d5939 --- /dev/null +++ b/4.4.0/Getting-Started/Quick-Start-Guide/index.html @@ -0,0 +1,2423 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Quick Start Guide - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Quick Start Guide

+ +

This quick start guide will help you get a RomM instance up and running. It is split into 3 parts: Prepare, Build and Configure.

+

Prepare

+

There are a few things you need to have in place before you can start:

+ + +
+

Warning

+

RomM works without a metadata API for basic use, but may cause issues with plugins like Playnite. Setting up IGDB API keys is recommended to prevent setup problems.

+
+

Build

+

Now that we have everything gathered, we can begin getting your instance set up!

+
    +
  1. Download a copy of the latest docker-compose.example.yml file from GitHub
  2. +
  3. Edit the file and modify the following values to configure the database
      +
    • MARIADB_ROOT_PASSWORD: Sets the root password of the database. Use a unique and secure password (use a password generator for simplicity)
    • +
    • MARIADB_DATABASE: Sets the database name for RomM. This can be modified - but it's not necessary
    • +
    • MARIADB_USER: User to connect to the database with. This can be modified - but it's not necessary
    • +
    • MARIADB_PASSWORD: Password for the user to connect to the database with. Use a unique and secure password (use a password generator for simplicity)
    • +
    +
  4. +
  5. Modify the following values in the environment to configure the application. -- Other values can be changed, but should not be done unless you know what you are doing, and are outside the scope of this guide
      +
    • DB_NAME: Name of the database set in the database section
    • +
    • DB_USER: Name of the user to connect to the database
    • +
    • DB_PASSWD: Password of the user to connect to the database
    • +
    +
  6. +
  7. Run the following command in a terminal and save the output to the ROMM_AUTH_SECRET_KEY environment variable: +
    openssl rand -hex 32
    +
    + It should look something like this: +
    03a054b6ca27e0107c5eed552ea66becd9f3a2a8a91e7595cd462a593f9ecd09
    +
  8. +
  9. Add your metadata sources API keys:
      +
    • IGDB: IGDB_CLIENT_ID and IGDB_CLIENT_SECRET
    • +
    • ScreenScraper.fr: SCREENSCRAPER_USER and SCREENSCRAPER_PASSWORD
    • +
    • RetroAchievements: RETROACHIEVEMENTS_API_KEY
    • +
    • MobyGames: MOBYGAMES_API_KEY
    • +
    • SteamGridDB: STEAMGRIDDB_API_KEY
    • +
    • Hasheous: HASHEOUS_API_ENABLED=true
    • +
    • PlayMatch: PLAYMATCH_API_ENABLED=true
    • +
    • LaunchBox: LAUNCHBOX_API_ENABLED=true
    • +
    +
  10. +
  11. Modify the following values in the volumes to configure the application
      +
    • /path/to/library: Path to the directory where your ROM files will be stored (usually the parent folder of the roms folder)
    • +
    • /path/to/assets: Path to the directory where you will store your save files and other assets
    • +
    • /path/to/config: Path to the directory where you will store the config.yml (store the config.yml file in this folder)
    • +
    +
  12. +
  13. +

    Save the file as docker-compose.yml instead of docker-compose.example.yml. It should look something like this:

    + +
    +Example Docker Compose +
    version: "3"
    +
    +volumes:
    +    mysql_data:
    +    romm_resources:
    +    romm_redis_data:
    +
    +services:
    +    romm:
    +        image: rommapp/romm:latest
    +        container_name: romm
    +        restart: unless-stopped
    +        environment:
    +            - DB_HOST=romm-db
    +            - DB_NAME=romm # Should match MARIADB_DATABASE in mariadb
    +            - DB_USER=romm-user # Should match MARIADB_USER in mariadb
    +            - DB_PASSWD= # Should match MARIADB_PASSWORD in mariadb
    +            - ROMM_AUTH_SECRET_KEY= # Generate a key with `openssl rand -hex 32`
    +            - SCREENSCRAPER_USER= # These are the recommended metadata providers
    +            - SCREENSCRAPER_PASSWORD= # https://docs.romm.app/latest/Getting-Started/Metadata-Providers/#screenscraper
    +            - RETROACHIEVEMENTS_API_KEY= # https://docs.romm.app/latest/Getting-Started/Metadata-Providers/#retroachievements
    +            - STEAMGRIDDB_API_KEY= # https://docs.romm.app/latest/Getting-Started/Metadata-Providers/#steamgriddb
    +            - HASHEOUS_API_ENABLED=true # https://docs.romm.app/latest/Getting-Started/Metadata-Providers/#hasheous
    +        volumes:
    +            - romm_resources:/romm/resources # Resources fetched from IGDB (covers, screenshots, etc.)
    +            - romm_redis_data:/redis-data # Cached data for background tasks
    +            - /path/to/library:/romm/library # Your game library. Check https://github.com/rommapp/romm?tab=readme-ov-file#folder-structure for more details.
    +            - /path/to/assets:/romm/assets # Uploaded saves, states, etc.
    +            - /path/to/config:/romm/config # Path where config.yml is stored
    +        ports:
    +            - 80:8080
    +        depends_on:
    +            romm-db:
    +                condition: service_healthy
    +                restart: true
    +
    +    romm-db:
    +        image: mariadb:latest
    +        container_name: romm-db
    +        restart: unless-stopped
    +        environment:
    +            - MARIADB_ROOT_PASSWORD= # Use a unique, secure password
    +            - MARIADB_DATABASE=romm
    +            - MARIADB_USER=romm-user
    +            - MARIADB_PASSWORD=
    +        volumes:
    +            - mysql_data:/var/lib/mysql
    +        healthcheck:
    +            test: [CMD, healthcheck.sh, --connect, --innodb_initialized]
    +            start_period: 30s
    +            start_interval: 10s
    +            interval: 10s
    +            timeout: 5s
    +            retries: 5
    +
    +
    +
  14. +
  15. +

    Open the terminal and navigate to the directory containing the docker-compose file

    +
  16. +
  17. +

    Run docker compose up -d to kick off the docker pull. You will see it pull the container and set up the volumes and network:

    +
        {
    +        "file": "../latest/resources/asciinema/quick-start-docker-compose.cast",
    +        "title": "RomM docker compose install",
    +        "preload": true,
    +        "loop": true,
    +        "auto_play": true,
    +        "cols": 140,
    +        "rows": 30,
    +        "fit": "width",
    +        "terminal_font_size": "small",
    +        "terminal_line_height": "1.2",
    +        "terminal_font_family": "Roboto Mono, Monaco, Consolas, monospace"
    +    }
    +
    +
  18. +
  19. +

    Run docker ps -f name=romm to verify that the containers are running

    +
  20. +
  21. Open a web browser and navigate to http://localhost:80, where you should be greeted with the RomM setup page
  22. +
  23. Go through the setup wizard, setting your admin username and password
  24. +
  25. Log in with the credentials you set in the last step
  26. +
+

Configure

+

Importing your ROMs via scanner

+

This method is generally the fastest and recommended for first time setup. You need your library properly mounted as a container volume:

+
    +
  1. Log into RomM with your user credentials
  2. +
  3. Click the Scan button in the sidebar
  4. +
  5. Select the metadata providers you want to fetch metadata from
  6. +
  7. The system will now begin scanning the ROM files and applying metadata to them. You can click on any of the items that it has tagged to see the metadata it pulled without having to stop the scan
  8. +
  9. After the scan completes, click the RomM logo to go back to the main screen. You should see the platforms and recent games it has scanned. You are now ready to rock with RomM!
  10. +
+

Uploading your ROMs via Web Interface

+

This method is certainly viable, but not recommended if you have a lot of ROMs and/or multiple platforms. It is good for adding files after the fact as your collection grows, but wouldn't be recommended for the first setup, nor for multi-file ROMs:

+
    +
  1. Log into RomM with your user credentials
  2. +
  3. Click the Upload button in the sidebar
  4. +
  5. Select the platform, then click + ADD and select the ROMs you want to upload in the file selector that appears
  6. +
  7. Click Upload to begin uploading the ROMs
  8. +
  9. Repeat for all the roms/platforms you have
  10. +
+

upload dialog

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Getting-Started/Reverse-Proxy/index.html b/4.4.0/Getting-Started/Reverse-Proxy/index.html new file mode 100644 index 0000000..0fb3a05 --- /dev/null +++ b/4.4.0/Getting-Started/Reverse-Proxy/index.html @@ -0,0 +1,2552 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Reverse Proxy - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Reverse Proxy

+ +

Here are some basic configurations for popular reverse proxies. Additional configuration may be required depending on your specific setup.

+

Caddy

+
http://romm.mysite.com {
+  reverse_proxy romm:8080
+}
+
+

Caddy + TLS (HTTPS)

+
https://romm.mysite.com {
+  tls mysite.com.crt mysite.com.key  # Certificate and key files
+
+  encode zstd gzip
+
+  header * {
+    Strict-Transport-Security "max-age=31536000;"
+    X-XSS-Protection "1; mode=block"
+    X-Frame-Options "SAMEORIGIN"
+    X-Robots-Tag "noindex, nofollow"
+    -Server
+    -X-Powered-By
+  }
+
+  reverse_proxy romm:8080
+}
+
+

Nginx

+
server {
+  listen 80 default_server;
+  server_name romm.mysite.com;
+  client_max_body_size 0;
+
+  location / {
+    include /config/nginx/proxy.conf;
+    include /config/nginx/resolver.conf;
+    set $upstream_app romm;
+    set $upstream_port 8080;
+    set $upstream_proto http;
+    proxy_pass $upstream_proto://$upstream_app:$upstream_port;
+  }
+}
+
+

Nginx + TLS (HTTPS)

+
server {
+  listen 80 default_server;
+  server_name _;
+  return 301 https://$host$request_uri;
+}
+
+server {
+    listen 443 ssl http2;
+    listen [::]:443 ssl http2;
+
+    server_name romm.mysite.com;
+    include /config/nginx/ssl.conf;
+    client_max_body_size 0;
+
+    location / {
+      include /config/nginx/proxy.conf;
+      include /config/nginx/resolver.conf;
+      set $upstream_app romm;
+      set $upstream_port 8080;
+      set $upstream_proto http;
+      proxy_pass $upstream_proto://$upstream_app:$upstream_port;
+
+      # Hide version
+      server_tokens off;
+
+      # Security headers
+      add_header X-Frame-Options "SAMEORIGIN" always;
+      add_header X-Content-Type-Options "nosniff" always;
+      add_header X-XSS-Protection "1; mode=block" always;
+      add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+      add_header Referrer-Policy "no-referrer-when-downgrade" always;
+    }
+}
+
+

Nginx Proxy Manager

+

Items marked with ❗ are important to set, as RomM may not work correctly otherwise!

+

⚡ Details

+
    +
  • Domain Names: romm.example.com (replace example with your own)
  • +
  • Scheme: http
  • +
  • Forward Hostname/IP: device IP (like 192.168.X.X)
  • +
  • Forward Port: 8080
  • +
  • Cache Assets: off
  • +
  • Block Common Exploits: on
  • +
  • Websockets Support: on ❗
  • +
  • Access List: - (or configure as needed)
  • +
+

🛡️ SSL

+

Strongly recommended for security; enable this if you plan to use HTTPS.

+
    +
  • SSL Certificate: "Request a new SSL Certificate"
  • +
  • Force SSL: on
  • +
  • HTTP/2 Support: on
  • +
  • HSTS Enabled: off
  • +
  • Email Address for Let's Encrypt: your email address
  • +
  • I Agree to the TOS: on
  • +
+

⚙️ Advanced

+

Custom Nginx Configuration ❗

+
proxy_max_temp_file_size 0;
+
+ + + + + + + + + + + + + + + +
DetailsSSLAdvanced
imageimage2image3
+

Traefik

+

Using a configuration document

+
http:
+    romsdomainse:
+        entryPoints:
+            - "https"
+        rule: "Host(`roms.domain.se`)"
+        middlewares:
+            - default-headers
+            - https-redirectscheme
+        tls:
+            certResolver: http
+        service: romsdomainse
+
+services:
+    romsdomainse:
+        loadBalancer:
+            servers:
+                - url: "http://192.168.1.100:8080"
+            passHostHeader: true
+
+

Using labels in docker compose

+
labels:
+    - "traefik.enable=true"
+    - "traefik.http.services.romm.loadbalancer.server.port=8080"
+    - "traefik.http.routers.romm.rule=Host(`romm.YOUR_DOMAIN.com`)"
+    - "traefik.http.routers.romm.entrypoints=websecure"
+    - "traefik.http.routers.romm.tls=true"
+    - "traefik.http.routers.romm.tls.certresolver=https"
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Integrations/Playnite-plugin/index.html b/4.4.0/Integrations/Playnite-plugin/index.html new file mode 100644 index 0000000..d770d1e --- /dev/null +++ b/4.4.0/Integrations/Playnite-plugin/index.html @@ -0,0 +1,2381 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Playnite plugin - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Playnite plugin

+ +
+ romm[playnite] isotipo +
+ +

Playnite is an open source video game library manager with one simple goal: To provide a unified interface for all of your games.

+

This plugin allows you to import your RomM library into Playnite. It queries the RomM API to create Playnite library entires for each of your games. Installing a game in Playnite will download it from RomM and store it on your system, allowing you to launch it in your emulator of choice.

+

Installation

+
    +
  • Option A: Open this link in your browser to launch Playnite and install the plugin automatically: playnite://playnite/installaddon/RomM_9700aa21-447d-41b4-a989-acd38f407d9f
  • +
  • Option B: Download the plugin from the Playnite add-ons website
  • +
  • Option C: In Playnite, go to Menu -> Add-ons... -> Browse -> Libraries, search for RomM, and click Install
  • +
  • Option D: Download the latest release from the releases page and install it manually by dragging the .pext file onto Playnite
  • +
+

Setup

+

Emulators

+

The plugin requires that you have at least 1 emulator installed on your system and configured in Playnite. You can use a built-in emulator or a custom one. If no emulators are installed and configured, you won't be able to complete setup! To set up an emulator, go to Menu -> Library -> Configure Emulators... -> Add emulator....

+

Settings

+

The plugin needs to be configured before it can be used. To do this, go to Menu -> Library -> Configure Integrations... -> RomM.

+

Authentication

+

You'll need to enter the host URL of your RomM instance, as well as a username and password. Passwords are stored in plaintext in Playnite, so it's recommended to use a separate account with the "VIEWER" role. The host URL has the include the protocol (http/https) and should not include a trailing slash, e.g. https://romm.example.com.

+

Emulator path mappings

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExampleRequired
EmulatorA built-in (or custom) emulatorDolphin✓
Emulator ProfileA built-in (or custom) emulator profileNintendo GameCube✓
PlatformThe platform or consoleNintendo GameCube✓
Destination PathThe path where downloaded ROMs will be storedC:\roms\gc✓
Auto-extractWhether compressed files should be extracted automatically
EnabledWhether the mapping is enabled
+

Importing your library

+

Once you've set up the plugin, you can import your library by going to Menu -> Library -> Import RomM library. All games matching the emulator path mappings will be imported into Playnite.

+

Installing a game will download it from RomM and store it in the destination path. You can then launch the game from Playnite, and it will be launched using the configured emulator.

+

By default, compressed files will be extracted automatically into a folder matching the game's name. You can modify this behavior in the settings page.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Integrations/Tinfoil-integration/index.html b/4.4.0/Integrations/Tinfoil-integration/index.html new file mode 100644 index 0000000..2933baf --- /dev/null +++ b/4.4.0/Integrations/Tinfoil-integration/index.html @@ -0,0 +1,2290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tinfoil Integration - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Tinfoil Integration

+ +
+ romm[tinfoil] isotipo +
+ +

Tinfoil is a homebrew application for the Nintendo Switch, primarily used for installing and managing software, including games and updates, that are not obtained through official channels.

+

This will help you configure Tinfoil for your Switch to work with your RomM library.

+

Setup

+

Prepare

+

Please note down the following in order to make this as smooth as possible, as well as some pre-reqs:

+
    +
  • RomM updated to at least 3.5.0
  • +
  • Add DISABLE_DOWNLOAD_ENDPOINT_AUTH=true to your environment variables and restart the container
  • +
  • The URL you use to access RomM
      +
    • This can either be http or https
    • +
    • The system will prefer local access though to avoid reverse proxy issues
    • +
    +
  • +
  • Feed URL: /api/tinfoil/feed
  • +
  • The username and password you use to login to RomM
  • +
+

Configure

+

Now it's time to configure your switch - Please follow the steps, this will assume you have Tinfoil installed and know how to use the basic functions of it.

+
    +
  1. Open Tinfoil and go to File Browser
  2. +
  3. Scroll over to the selection and press - in order to access the new menu.
  4. +
  5. Enter these Options
      +
    • Protocol - http or https depending on your connection
    • +
    • Host - Host of your RomM instance
    • +
    • Port - Port of your RomM Instance
    • +
    • Path - /api/tinfoil/feed
    • +
    • Username - Username of your RomM instance
    • +
    • Password - Password of your RomM instance
    • +
    • Title - Free text title, make it whatever you want.
    • +
    • Enabled - Yes
    • +
    +
  6. +
  7. Press X to save
  8. +
  9. Now close out of Tinfoil and go back in, so it can scan the TitleIDs - if everything is correct you will have this custom motd: + " RomM Switch Library"
  10. +
+

Now you will be able to see the files in "New Games" tab of Tinfoil OR you can access it within the "File Browser" section that you setup earlier.

+

Image of Tinfoil

+

Additional

+

It didn't pull anything through to "New Games" and has not parsed any information about the titles?!

+

That would be because the filename it has tried to pull had no TitleID (Improvement to RomM coming soon ™)

+

Make sure the filename has the TitleID within the title like this: +TitleID

+

Once this is done, the next time Tinfoil is opened it is always parsed and re-scanned.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Integrations/muOS-app/index.html b/4.4.0/Integrations/muOS-app/index.html new file mode 100644 index 0000000..627733e --- /dev/null +++ b/4.4.0/Integrations/muOS-app/index.html @@ -0,0 +1,2252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + muOS app - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

muOS app

+ +
+ romm[muos] isotipo +
+ +

muOS is a Custom Firmware (CFW) primarily for handheld devices. Configurable, themeable, friendly, easy-to-use.

+

The muOS app connects to your RomM instance and allows you to fetch games wirelessly from your Anbernic device.

+

Setup

+

muOS

+

We leverage the muOS Archive Manager to install/update the app.

+
    +
  1. Head to the latest release and download the romm_muOS_install_x.x.x.zip file.
  2. +
  3. Move the compressed ZIP file to /mnt/mmc/ARCHIVE on your device.
  4. +
  5. Launch the manager from Applications > Archive Manager and select romm_muOS_install_x.x.x.zip.
  6. +
  7. Once installed, make a copy of /mnt/mmc/MUOS/application/RomM/env.template, rename it to /mnt/mmc/MUOS/application/RomM/.env, edit it (any method is fine, we recommend SSH) and set HOST, USERNAME and PASSWORD.
  8. +
  9. Launch the app from Applications > RomM and start browsing your collection.
  10. +
+

EmulationStation

+

We use PortMaster to install the app on devices running EmulationStation.

+
    +
  1. Download the RomM App.sh file and RomM/ folder to the roms/ports on your device.
  2. +
  3. Make the RomM App.sh file executable by running chmod +x RomM App.sh.
  4. +
  5. Launch EmulationStation and navigate to the Ports section.
  6. +
+ +
+

Note

+

Your device must connect to your RomM instance or home server over Wi-Fi. The easiest method is to keep them on the same network and set HOST to the server's IP and the port where RomM is running. Advanced users or those using reverse proxies can configure their network and DNS settings as needed, using a URL with a domain like https://romm.domain.com.

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Maintenance/Migrating-to-new-machine/index.html b/4.4.0/Maintenance/Migrating-to-new-machine/index.html new file mode 100644 index 0000000..1a354a9 --- /dev/null +++ b/4.4.0/Maintenance/Migrating-to-new-machine/index.html @@ -0,0 +1,2277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Migrating RomM - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Migrating RomM

+ +

Migrating RomM to a new system

+

Migrating RomM to a new system is possible, but all of the docker volumes must be copied for RomM to run correctly.

+

Following the the setup in the Quick Start Guide these volumes are created be default

+

RomM should be stopped before following this guide.

+
volumes:
+    mysql_data:
+    romm_resources:
+    romm_redis_data:
+
+services:
+    romm:
+        volumes:
+            - romm_resources
+            - romm_redis_data
+    romm-db:
+        volumes:
+            - mysql_data
+
+

These volumes will need to manually moved to the new system. This is a straightforward process that includes determining their location and then copying them.

+

Determining the docker root directory and copying the volumes

+
    +
  1. First determine the docker root directory
  2. +
+
docker info | grep 'Docker Root Dir'
+
+

The expected output on a standard linux system:

+
Docker Root Dir: /var/lib/docker
+
+
    +
  1. Double check that the volumes have been created by docker and are owned by the docker engine
  2. +
+
docker volume ls
+
+

Following the default quick start guide the following volumes will have been made

+
DRIVER    VOLUME NAME
+local     romm_mysql_data
+local     romm_romm_redis_data
+local     romm_romm_resources
+
+
    +
  1. Inspect each volume to get the exact location of the volume data
  2. +
+
docker volume inspect romm_mysql_data | grep Mountpoint
+
+
    +
  • The output of the docker inspect will return the exact storage location of the volumes data
  • +
+
"Mountpoint": "/var/lib/docker/volumes/romm_mysql_data/_data",
+
+
    +
  1. Copy those volumes into a new location so that they can be safely migrated to a new system each volume needs to be in its own folder
  2. +
+
cp -r /var/lib/docker/volumes/romm_mysql_data/_data/ /your/new/path/romm_mysql_data
+
+cp -r /var/lib/docker/volumes/romm_romm_redis_data/_data /your/new/path/romm_romm_redis_data
+
+cp -r /var/lib/docker/volumes/romm_romm_resources/_data /your/new/path/romm_romm_resources
+
+
    +
  1. Update the docker-compose.yml volume paths with the newly copied data to determine RomM still loads correctly.
  2. +
+
services:
+    romm:
+        volumes:
+            - /your/new/path/romm_romm_resources # romm_resources
+            - /your/new/path/romm_romm_redis_data # romm_redis_data
+    romm-db:
+        volumes:
+            - /your/new/path/romm_mysql_data # mysql_data
+
+

If RomM starts up correctly, then it is safe to copy all of your RomM folders to a new system.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Maintenance/Scheduled-Tasks/index.html b/4.4.0/Maintenance/Scheduled-Tasks/index.html new file mode 100644 index 0000000..5ec0fe4 --- /dev/null +++ b/4.4.0/Maintenance/Scheduled-Tasks/index.html @@ -0,0 +1,2336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Scheduled Tasks - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Scheduled Tasks

+ +

Scheduled tasks

+

Scheduled tasks can be enabled and configured with the following environment variables:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDescriptionValue
ENABLE_SCHEDULED_RESCANEnable scheduled re-scanning of librarytrue
SCHEDULED_RESCAN_CRONCron expression for scheduled re-scanning"0 3 * * *"
ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDBEnable scheduled updating of Switch TitleDB indextrue
SCHEDULED_UPDATE_SWITCH_TITLEDB_CRONCron expression for scheduled updating of Switch TitleDB"0 4 * * *"
ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATAEnable scheduled updating of LaunchBox metadatatrue
SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON.Cron expression for scheduled updating of LaunchBox metadata"0 5 * * *"
+

Scheduled re-scan

+

Users can opt to enable scheduled re-scans, and set the interval using Cron notation. Not that the scan will not completely re-scan every file, only catching those which have been added/updated.

+

Switch titleDB update

+

Support was added for Nintendo Switch ROMs with filenames using the titleid/programid format (e.g. 0100000000010000.xci). If a file under the switch folder matches the regex, the scanner will use the index to attempt to match it to a game. If a match is found, the IGDB handler will use the matched name as the search term.

+

The associated task updates the /fixtures/switch_titledb.json file at a regular interval to support new game releases.

+

LaunchBox metadata update

+

RomM can also update the LaunchBox metadata database at a regular interval. This is onyl useful if you've enabled the LaunchBox metadata provider. The database is updated by downloading the latest version from the LaunchBox Games Database and replacing the existing one.

+

File system watcher

+

RomM can also monitor the filesystem for events (files created/moved/deleted) and schedules a re-scan of the platform (or entire library is a new platform was added).

+

The watcher can be enabled and configured with the following environment variables:

+ + + + + + + + + + + + + + + + + + + + +
VariableDescriptionValue
ENABLE_RESCAN_ON_FILESYSTEM_CHANGEEnable re-scanning of library when filesystem changestrue
RESCAN_ON_FILESYSTEM_CHANGE_DELAYDelay in minutes before re-scanning library when filesystem changes5
+

The watcher will monitor the /library/roms folder for changes to the filesystem, such as files being added, moved or deleted. It will ignore certain events (like modifying the file content or metadata), and will skip default OS files (like .DS_Store on mac).

+

When a change is detected, a scan will be scheduled for sometime in the future (default 5 minutes). If other events are triggered between now and the time at which the scan starts, more platforms will be added to the scan list (or the scan may switch to a full scan). This is done to reduce the number of tasks scheduled when many big changes happen to the library (mass upload, new mount, etc.)

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Maintenance/Upgrading-to-3.0/index.html b/4.4.0/Maintenance/Upgrading-to-3.0/index.html new file mode 100644 index 0000000..40e744e --- /dev/null +++ b/4.4.0/Maintenance/Upgrading-to-3.0/index.html @@ -0,0 +1,2281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Upgrading to 3.0 - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Upgrading to 3.0

+ +

Version 3.0 of RomM introduces a number of breaking changes aimed at improving performance and usability, which will require some users to make specific changes before upgrading to ensure compatibility and to take full advantage of the new features.

+

All of the following changes are reflected in the example docker-compose.yml file, which has been simplified greatly. Please read this entire file carefully, as failing to do so may cause RomM to become inaccessible or unresponsive.

+

Dropped support for SQLite

+

We're removed support for SQLite as we've faced a number of engineering issues with it in the past, and MariaDB has proven more stable and robust. If you currently use SQLite, we'll automatically migrate your data from SQLite to MariaDB, but you'll first need to make the following changes before upgrading to the latest image.

+

In your environment variables, change ROMM_DB_DRIVER to mariadb (or remove it completely as it's no longer needed). You'll then want to add the following environment variables:

+
- DB_HOST=mariadb
+- DB_PORT=3306
+- DB_NAME=romm # Should match MYSQL_DATABASE in mariadb
+- DB_USER=romm-user # Should match MYSQL_USER in mariadb
+- DB_PASSWD= # Should match MYSQL_PASSWORD in mariadb
+
+

To setup a new MariaDB container, have a look at the example docker-compose.yml file.

+

Authentication as standard

+

To support new features like EmulatorJS and saves/states management, we've decided to require authentication for all users. Anyone currently running RomM with authentication disabled will need to remove the ROMM_AUTH_ENABLED environment variable and add the following ones:

+
- ROMM_AUTH_SECRET_KEY= # Generate a key with `openssl rand -hex 32`
+
+

We understand that this requirement for authentication might conflict with the way some users currently share their collection with others (unrestricted access for all). However, given the exciting new features we've built, and the ones we're looking to build in the near future, we feel this is the right decision for the project.

+

Redis is now built-in

+

As Redis is required for authentication to work, we've integrated it directly into the docker image. If you're currently running the experimental Redis container, you can remove it, along with these environment variables:

+
- ENABLE_EXPERIMENTAL_REDIS
+- REDIS_HOST
+- REDIS_PORT
+
+

Configuration folder

+

Mounting the config.yml file is now done by mounting a config folder.. Place your existing config.yml file inside a folder and bind it to /romm/config:

+
- /path/to/config:/romm/config
+
+

Updated config.example.yml

+

Support for saves, states and screenshots

+

This version introduces preliminary support for uploading/downloading saves, states and screenshots (read more about it in the 3.0 release notes). We've added a new volume mapping for these types of files called assets, which you'll want to bind to a local folder (or volume) so they'll persist. In your volumes section, add the following mapping, where /path/to/assets/ is some folder where you'll want to store these assets (and make sure that folder exists):

+
- /path/to/assets:/romm/assets
+
+

We recommend creating a folder next to your library/the one mapped to /romm/library in order to keep all your RomM files in the same place.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Miscellaneous/Brand-Guidelines/index.html b/4.4.0/Miscellaneous/Brand-Guidelines/index.html new file mode 100644 index 0000000..9dc508e --- /dev/null +++ b/4.4.0/Miscellaneous/Brand-Guidelines/index.html @@ -0,0 +1,2251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Branding Guidelines - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Branding Guidelines

+ +

We’ve put together some guidelines for those anyone who wants to use our name and logo. In this context, "RomM", "The RomM Project", "the project", "we", "us", and "our" refer to the RomM project.

+ +
+ romm isotipo +
+
+ +The logo should always be used in its standard colors: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ColorHex Code
#371f69#371f69
#553e98#553e98
#ede5f8#ede5f8
#bea4e1#bea4e1
#e6c7a7#e6c7a7
#e1a38d#e1a38d
+
+ +

Do these things

+
    +
  • Use our logo to link to any page or site owned or operated by the project.
  • +
  • Use our logo in a blog post or news article about the project.
  • +
  • Use our logo to inform others that your project integrates with RomM.
  • +
  • Always use our logo in the colors provided.
  • +
  • Always use our name in a way that makes it clear you are not affiliated with the project.
  • +
+

If you're working on a project that integrates with RomM and would like to use/remix the logo, please reach out to us first. We'd love to hear about what you're working on!

+

Please don't do these things

+
    +
  • Use our name or logo in any way that would suggest you are us, are endorsed by us, or are part of the project.
  • +
  • Use our name or logo in a way that implies partnership, sponsorship, or endorsement.
  • +
  • Use our name or logo as the name or logo for your project, product, service, social media account, company, or website.
  • +
  • Use our name or logo to promote, advertise or sell any private business, closed-source software, commercial product, or paid service.
  • +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Miscellaneous/FAQs/index.html b/4.4.0/Miscellaneous/FAQs/index.html new file mode 100644 index 0000000..865adad --- /dev/null +++ b/4.4.0/Miscellaneous/FAQs/index.html @@ -0,0 +1,2238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FAQs - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

FAQs

+ +

When will the next version of RomM be available?

+

When it's ready.

+

When will the version after that one release?

+

After the upcoming version is released.

+

When will X feature be available?

+

Sometime between now and the heat death of the universe.

+

When will version x.x.x of RomM (or any of the RomM clients/apps/plugins) be released?

+

Same as above question.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Navigation/index.html b/4.4.0/Navigation/index.html new file mode 100644 index 0000000..790d1a1 --- /dev/null +++ b/4.4.0/Navigation/index.html @@ -0,0 +1,2152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Navigation - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Navigation

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/OIDC-Guides/OIDC-Setup-With-Authelia/index.html b/4.4.0/OIDC-Guides/OIDC-Setup-With-Authelia/index.html new file mode 100644 index 0000000..ecf9d6b --- /dev/null +++ b/4.4.0/OIDC-Guides/OIDC-Setup-With-Authelia/index.html @@ -0,0 +1,2444 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + With Authelia - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

OIDC Setup With Authelia

+

A quick rundown of the technologies

+

What is Authelia?

+

Authelia is an open-source authentication and authorization server providing two-factor authentication and single sign-on (SSO) for your applications via a web portal. It acts as a companion for reverse proxies by allowing, denying, or redirecting requests. Authelia can be deployed alongside your other services to centralize identity management.

+

What is OAuth2?

+

OAuth2 (Open Authorization 2.0) is an industry-standard protocol for authorization. It allows applications (clients) to gain limited access to user accounts on an HTTP service without sharing the user’s credentials. Instead, it uses access tokens to facilitate secure interactions. OAuth2 is commonly used in scenarios where users need to authenticate via a third-party service.

+

What is OpenID Connect (OIDC)?

+

OIDC (OpenID Connect) is an identity layer built on top of OAuth2. While OAuth2 primarily handles authorization, OIDC adds authentication, enabling applications to verify a user’s identity and obtain profile information. This makes OIDC suitable for SSO solutions, where user identity is central to access management.

+

Setting up a Provider and Application in Authelia

+

Step 1: Install and Configure Authelia

+

Before setting up a provider and app, ensure that Authelia is installed and running by following the getting started and OIDC provider guides.

+

Step 2: Add a client

+

First, in Authelia's configuration.yml, at identity_providers → oidc → claims_policies you'll need to add a Claims Policy if you do not already have one with the specified claims:

+
# identity_providers:
+#   oidc:
+claims_policies:
+    with_email: # You can name this however you want
+        id_token:
+            [
+                "email",
+                "email_verified",
+                "alt_emails",
+                "preferred_username",
+                "name",
+            ]
+
+

To read more about claims_policies and why you need it for RomM, see this section in the Authelia docs.

+

Then, in the same configuration.yml, under identity_providers → oidc → clients, add a new entry:

+
    +
  • A random client_id and client_secret +
  • +
  • public should be set to false.
  • +
  • redirect_uris should include your RomM instance's URL + /api/oauth/openid (e.g., http://romm.host.local/api/oauth/openid).
  • +
  • claims_policy is the name of the entry at claims_policies that you just added (or already had).
  • +
  • scopes includes openid, email and profile.
  • +
  • token_endpoint_auth_method should be set to client_secret_basic.
  • +
  • userinfo_signed_response_alg should be set to none.
  • +
+

Refer to the official docs for more details.

+

This entry should look like this:

+
#identity_providers:
+#  oidc:
+#    clients:
+- client_id: "<randomly_generated>" # read above for how generate
+  client_name: "RomM" # will be displayed in Authelia to users
+  client_secret: "$pbkdf2-sha512$randomly_generated" # read above for how generate
+  public: false
+  authorization_policy: "two_factor" # or one_factor, depending on your needs
+  grant_types:
+      - authorization_code
+  redirect_uris:
+      - "http://romm.host.local/api/oauth/openid"
+  claims_policy: "with_email"
+  scopes:
+      - "openid"
+      - "email"
+      - "profile"
+  userinfo_signed_response_alg: "none"
+  token_endpoint_auth_method: "client_secret_basic"
+
+

Step 3: Configure RomM Environment Variables

+

To enable OIDC authentication in RomM, you need to set the following environment variables:

+
    +
  • OIDC_ENABLED: Set to true to enable OIDC authentication.
  • +
  • OIDC_PROVIDER: The lowercase name of the provider (authelia).
  • +
  • OIDC_CLIENT_ID: The client ID copied from the Authelia application.
  • +
  • OIDC_CLIENT_SECRET: The generated output from Random Password.
  • +
  • OIDC_REDIRECT_URI: The redirect URI configured in the Authelia provider, in the format http://romm.host.local/api/oauth/openid.
  • +
  • OIDC_SERVER_APPLICATION_URL: The base URL for you Authelia instance, e.g. http://authelia.host.local.
  • +
+

Step 4: Set your Email in RomM

+

In RomM, open your user profile and set your email address. This email has to match your user email in Authelia.

+

Set email

+

Step 5: Test the Integration

+

After configuring the environment variables, restart (or stop and remove) your RomM instance and navigate to the login page. You should see an option to log in using OIDC. Click on the OIDC button, and you'll be redirected to Authelia for authentication. Once authenticated, you'll be redirected back to RomM.

+

Login with OIDC

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/OIDC-Guides/OIDC-Setup-With-Authentik/index.html b/4.4.0/OIDC-Guides/OIDC-Setup-With-Authentik/index.html new file mode 100644 index 0000000..bbbfa95 --- /dev/null +++ b/4.4.0/OIDC-Guides/OIDC-Setup-With-Authentik/index.html @@ -0,0 +1,2459 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + With Authentik - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

OIDC Setup With Authentik

+

A quick rundown of the technologies

+

What is Authentik?

+

Authentik is an open-source identity provider (IdP) designed to manage authentication, authorization, and user management across applications. It supports modern authentication protocols and provides tools to simplify integration, including single sign-on (SSO), multi-factor authentication (MFA), and auditing capabilities. Authentik can be deployed alongside your other services to centralize identity management.

+

What is OAuth2?

+

OAuth2 (Open Authorization 2.0) is an industry-standard protocol for authorization. It allows applications (clients) to gain limited access to user accounts on an HTTP service without sharing the user’s credentials. Instead, it uses access tokens to facilitate secure interactions. OAuth2 is commonly used in scenarios where users need to authenticate via a third-party service.

+

What is OpenID Connect (OIDC)?

+

OIDC (OpenID Connect) is an identity layer built on top of OAuth2. While OAuth2 primarily handles authorization, OIDC adds authentication, enabling applications to verify a user’s identity and obtain profile information. This makes OIDC suitable for SSO solutions, where user identity is central to access management.

+

Setting up a Provider and Application in Authentik

+

Step 1: Install and Configure Authentik

+

Before setting up a provider and app, ensure that Authentik is installed and running by following the official installation guide..

+
    +
  1. Access Authentik via its web interface.
  2. +
  3. Log in as an administrator.
  4. +
  5. Navigate to the “Admin Interface” to configure the necessary components.
  6. +
+

Authentik user dashboard

+

Step 2: Create a Provider

+

A provider in Authentik acts as the bridge between RomM and Authentik.

+
    +
  1. Navigate to Providers:
      +
    • Go to the "Providers" section in the Authentik admin interface.
    • +
    +
  2. +
  3. Create a New Provider:
      +
    • Click on “Create” and choose the protocol (e.g., “OIDC Provider”). + Create a new provider
    • +
    +
  4. +
  5. Select "OAuth2/OpenID Provider" + Select OAuth2 provider
  6. +
  7. Configure Provider Settings:
      +
    • Name: Assign a unique name to the provider (e.g., "RomM OIDC Provider").
    • +
    • Authorization flow: Select implicit consent.
    • +
    • Redirect URIs: Add your RomM instance's URL + /api/oauth/openid (e.g., http://romm.host.local/api/oauth/openid).
    • +
    +
  8. +
  9. Copy the Client ID and Secret:
      +
    • You'll need these to set OIDC_CLIENT_ID and OIDC_CLIENT_SECRET in your RomM instance. + Provider settings
    • +
    +
  10. +
  11. Click Create.
  12. +
+

Step 3: Register an Application

+

An app in Authentik represents the external service (in our case RomM) that will use the provider for authentication.

+
    +
  1. Navigate to Applications:
      +
    • Go to the "Applications" section in the admin interface. + Applications
    • +
    +
  2. +
  3. Create a New Application:
      +
    • Click on “Create” and configure the app settings: - Name: Provide a recognizable name (e.g., "RomM"). - Slug: Create a unique identifier for the app (e.g., romm). - Provider: Link the app to the previously created provider, "RomM OIDC Provider". + New application
    • +
    +
  4. +
  5. Click Create.
  6. +
+

Step 4: Configure RomM Environment Variables

+

To enable OIDC authentication in RomM, you need to set the following environment variables:

+
    +
  • OIDC_ENABLED: Set to true to enable OIDC authentication.
  • +
  • OIDC_PROVIDER: The lowercase name of the provider (authentik).
  • +
  • OIDC_CLIENT_ID: The client ID copied from the Authentik application.
  • +
  • OIDC_CLIENT_SECRET: The client secret copied from the Authentik application.
  • +
  • OIDC_REDIRECT_URI: The redirect URI configured in the Authentik provider, in the format http://romm.host.local/api/oauth/openid.
  • +
  • OIDC_SERVER_APPLICATION_URL: The URL of the Authentik application, e.g., http://authentik.host.local/application/o/romm.
  • +
+

Step 5: Set your Email in RomM

+

In RomM, open your user profile and set your email address. This email has to match your user email in Authentik.

+

Set email

+

Step 6: Test the Integration

+

After configuring the environment variables, restart (or stop and remove) your RomM instance and navigate to the login page. You should see an option to log in using OIDC. Click on the OIDC button, and you'll be redirected to Authentik for authentication. Once authenticated, you'll be redirected back to RomM.

+

Login with OIDC

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/OIDC-Guides/OIDC-Setup-With-PocketID/index.html b/4.4.0/OIDC-Guides/OIDC-Setup-With-PocketID/index.html new file mode 100644 index 0000000..0e9dfcd --- /dev/null +++ b/4.4.0/OIDC-Guides/OIDC-Setup-With-PocketID/index.html @@ -0,0 +1,2408 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + With PocketID - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

OIDC Setup With Pocket ID

+

A quick rundown of the technologies

+

What is Pocket ID?

+

Pocket ID is a simple OIDC provider that allows users to authenticate with their passkeys to your services.

+

The goal of Pocket ID is to be a simple and easy-to-use. There are other self-hosted OIDC providers like Keycloak or ORY Hydra but they are often too complex for simple use cases.

+

Additionally, what makes Pocket ID special is that it only supports passkey authentication, which means you don’t need a password.

+

What is OAuth2?

+

OAuth2 (Open Authorization 2.0) is an industry-standard protocol for authorization. It allows applications (clients) to gain limited access to user accounts on an HTTP service without sharing the user’s credentials. Instead, it uses access tokens to facilitate secure interactions. OAuth2 is commonly used in scenarios where users need to authenticate via a third-party service.

+

What is OpenID Connect (OIDC)?

+

OIDC (OpenID Connect) is an identity layer built on top of OAuth2. While OAuth2 primarily handles authorization, OIDC adds authentication, enabling applications to verify a user’s identity and obtain profile information. This makes OIDC suitable for SSO solutions, where user identity is central to access management.

+

Setting up a client in Pocket ID

+

Step 1: Install and Configure Pocket ID

+

Before setting up the OIDC client, ensure that Pocket ID is installed and running by following the setup guide.

+

Step 2: Add a client

+

Once you have logged in and configured a PassKey you now need to create an OIDC client, this will let Pocket ID know about the application that needs to be configured, and will give you the relevant keys to add to the RomM compose file.

+
    +
  • Make sure within Application Configuration that "Emails Verified" is ticked, most OIDC apps will need this.
  • +
  • Go to OIDC Client
  • +
  • Click Add OIDC Client
      +
    • Name: RomM
    • +
    • Callback URLs: https://{host}/api/oauth/openid
    • +
    +
  • +
  • Click Save
  • +
  • Stay on this page, you will be shown your client secret only THIS time.
  • +
+

Step 3: Configure RomM Environment Variables

+

To enable OIDC authentication in RomM, you need to set the following environment variables:

+
    +
  • OIDC_ENABLED: Set to true to enable OIDC authentication.
  • +
  • OIDC_PROVIDER: The lowercase name of the provider (pocketid).
  • +
  • OIDC_CLIENT_ID: The client ID copied from the Pocket ID application
  • +
  • OIDC_CLIENT_SECRET: The client secret that is showing within your Pocket ID application.
  • +
  • OIDC_REDIRECT_URI: The redirect URI configured in the Pocket ID provider, in the format https://{host}/api/oauth/openid.
  • +
  • OIDC_SERVER_APPLICATION_URL: The authorization URL for you Pocket ID instance, e.g. https://id.host.local.
  • +
+

Step 4: Set your Email in RomM

+

In RomM, open your user profile and set your email address. This email has to match your user email in Pocket ID.

+

Set email

+

Step 5: Test the Integration

+

After configuring the environment variables, restart (or stop and remove) your RomM instance and navigate to the login page. You should see an option to log in using OIDC. Click on the OIDC button, and you'll be redirected to Pocket ID for authentication. Once authenticated, you'll be redirected back to RomM.

+

Login with OIDC

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/OIDC-Guides/OIDC-Setup-With-Zitadel/index.html b/4.4.0/OIDC-Guides/OIDC-Setup-With-Zitadel/index.html new file mode 100644 index 0000000..37cbadc --- /dev/null +++ b/4.4.0/OIDC-Guides/OIDC-Setup-With-Zitadel/index.html @@ -0,0 +1,2483 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + With Zitadel - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

OIDC Setup With Zitadel

+

A quick rundown of the technologies

+

What is Zitadel

+

Zitadel is an enterprise-grade, open-source identity and access management (IAM) platform that supports OAuth2, OpenID Connect, SAML, and passwordless authentication. It's used to manage users, roles, and secure login for web and cloud applications.

+

What is OAuth2?

+

OAuth2 (Open Authorization 2.0) is an industry-standard protocol for authorization. It allows applications (clients) to gain limited access to user accounts on an HTTP service without sharing the user’s credentials. Instead, it uses access tokens to facilitate secure interactions. OAuth2 is commonly used in scenarios where users need to authenticate via a third-party service.

+

What is OpenID Connect (OIDC)?

+

OIDC (OpenID Connect) is an identity layer built on top of OAuth2. While OAuth2 primarily handles authorization, OIDC adds authentication, enabling applications to verify a user’s identity and obtain profile information. This makes OIDC suitable for SSO solutions, where user identity is central to access management.

+

Setting up a client in Zitadel

+

Step 1: Install and Configure Zitadel

+

Before setting up the OIDC client, ensure that Zitadel is installed and running by following the setup guide.

+

Step 2: Create a Project

+

Once you have logged in and changed the default password for your Zitadel organization, create a new Project (i.e Romm). This will be the basic settings for roles and authorization.

+

In the "General" tab, there are options to allow the following:

+
    +
  • Assert Roles on Authentication
  • +
+

Unnecessary: Romm (at this time) does not allow granting permissions based on role, everyone gets viewer and will have to be changed manually using an admin account if desired.

+
    +
  • Check authorization on Authentication
  • +
+

Recommended: If you allow registration to your platform, then anyone who registers can instantly access Romm (although only as a viewer, which may not be a problem for some)

+
    +
  • Check for Project on Authentication
  • +
+

Optional: It could be used if you plan on separating users by organizations for other applications, but creating separate organizations is not typically useful for general self-hosting purposes

+

Step 2.5 (Optional: If you enabled "Check authorization on Authentication"): Grant user(s) access to the Project

+

Click on the Authorization tab and click New.

+

Enter the user(s) and click Continue

+

It should say "No role has been created yet.", but this is fine, you can just click Save and it should bring you back to the Authorization page with your user(s) listed with no roles

+

Step 3: Create the application

+

On the General tab, click the New button under Applications.

+

(Check "I'm a pro. Skip this wizard." to enter the information quicker)

+
    +
  • Name: RomM (or whatever you want)
  • +
  • Application Type: Web
  • +
  • Grant Types: Authorization Code
  • +
  • Response Types: Code
  • +
  • Authentication Method: Basic
  • +
  • Redirect URIs: https://romm.domain.com/api/oauth/openid
  • +
  • Post Logout URIs: https://romm.domain.com/
  • +
+

Click Create.

+
    +
  • Stay on this page or copy these down elsewhere, the secret will only show this one time
  • +
+

Step 3: Configure RomM Environment Variables

+

To enable OIDC authentication in RomM, you need to set the following environment variables:

+
    +
  • OIDC_ENABLED: Set to true to enable OIDC authentication.
  • +
  • OIDC_PROVIDER: The name of the provider Zitadel.
  • +
  • OIDC_CLIENT_ID: The client ID copied from the Zitadel application
  • +
  • OIDC_CLIENT_SECRET: The client secret generated from the Zitadel application
  • +
  • OIDC_REDIRECT_URI: The redirect URI configured in Zitadel https://rom.domain.com/api/oauth/openid.
  • +
  • OIDC_SERVER_APPLICATION_URL: The domain for your Zitadel instance https://zitadel.domain.com. (The discovery URL for Zitadel is on the basedomain under /.well-known/openid-configuration)
  • +
+

Step 4: Enable claims from ID Token (this resolves the "Email is missing from token" error)

+

Click close to finish creating the application and then go to the Token Settings tab.

+

Check "User Info inside ID Token" and click Save

+

Step 5: Set your Email in RomM

+

For your existing RomM admin account, open your user profile on Zitadel and set your email address. This email has to match your user email in Zitadel.

+

Step 6: Test the Integration

+

After configuring the environment variables, restart (or stop and remove) your RomM instance and navigate to the login page. You should see the option "LOGIN WITH ZITADEL". Click on it and you'll be redirected to Zitadel for authentication. Once authenticated, you'll be redirected back to RomM.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Platforms-and-Players/Custom-Platforms/index.html b/4.4.0/Platforms-and-Players/Custom-Platforms/index.html new file mode 100644 index 0000000..0c2b953 --- /dev/null +++ b/4.4.0/Platforms-and-Players/Custom-Platforms/index.html @@ -0,0 +1,2121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Custom Platforms - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Custom Platforms

+ +

While RomM supports every platform listed in the Supported Platforms page, the list is not exhaustive, and you may have ROMs in your library for other platforms. To load those files into RomM, place them in a folder for each platform, and give it a name that's all lowercase, with - to separate words, and with no white spaces. For example, pocket-challenge-v2 would map to Pocket Challenge V2, and display the default platform icon in the app.

+

Furthermore, only a portion of the supported platforms have custom icons built-in. If your library has platforms that aren't listed in the platforms icons list, RomM will display a default fallback icon.

+

If you'd like to load your own custom icons for missing platforms, you can mount /var/www/html/assets/platforms to some local folder and place all of your custom .ico platform icons in there. You'll also want to download the ones provided in this project and place them in the same folder. If you'd like to use your own icons for platforms already supported by RomM, just replace the file with another using the exact same name.

+

The name of the .ico file should match the slug of the platform on IGDB. For example, the URL for the AmstradCPC is https://www.igdb.com/platforms/acpc, so the filename should be acpc.ico.

+

Screenshot 2023-09-15 at 10 45 04 AM

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Platforms-and-Players/EmulatorJS-Player/index.html b/4.4.0/Platforms-and-Players/EmulatorJS-Player/index.html new file mode 100644 index 0000000..0bf7aef --- /dev/null +++ b/4.4.0/Platforms-and-Players/EmulatorJS-Player/index.html @@ -0,0 +1,2252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EmulatorJS - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

EmulatorJS

+ +

EmulatorJS is a web-based emulator for various system; that is, it allows you to run old games in your web browser. It's based on RetroArch compiled with Emscripten, which is a toolchain for compiling C and C++ code to WebAssembly.

+ +
+

Warning

+
    +
  • Emulation is a complex and resource-intensive process. As such, it may not work well in all browser, especially older or less powerful ones. If you're having trouble running a game, try using a different browser or device.
  • +
  • PSP emulation with the ppsspp core and MS-DOS with the dosbox-pure core are not currently supported when using the Console mode.
  • +
+
+ +
+

Note

+

Some platforms may require multiple BIOS/firmware files to be loaded at the same time. To do this, create a ZIP archive containing all the firmware files for the emulator you've selected and upload it to the firmware section of the platform. This ZIP file will be recognized by EmulatorJS as the firmware bundle for the platform. Refer to the EmulatorJS documentation for the required list of files for each platform.

+
+

Loading saves and states

+

Our integration with EmulatorJS automates the process of loading and save files and save states. Before starting the game, select a save and/or state file to load (if one is available). Anytime you manually save the game (or create a save state) by clicking the save or "save and quit" buttons, the save and state files stored with RomM will be updated, so there's no need to manually download or upload them.

+

Supported systems

+

Note that only the following systems are currently supported:

+
    +
  • 3DO
  • +
  • Amiga
  • +
  • Arcade/MAME
  • +
  • Atari 2600
  • +
  • Atari 5200
  • +
  • Atari 7800
  • +
  • Atari Jaguar
  • +
  • Atari Lynx
  • +
  • Commodore 64
  • +
  • ColecoVision
  • +
  • DOOM
  • +
  • Neo Geo Pocket
  • +
  • Neo Geo Pocket Color
  • +
  • MS-DOS
  • +
  • Nintendo 64
  • +
  • Nintendo Entertainment System (NES)
  • +
  • Nintendo Family Computer (Famicom)
  • +
  • Nintendo DS
  • +
  • Game Boy
  • +
  • Game Boy Color
  • +
  • Game Boy Advance
  • +
  • PC-FX
  • +
  • PlayStation (PS)
  • +
  • PlayStation Portable (PSP)
  • +
  • Sega 32X
  • +
  • Sega CD
  • +
  • Sega Game Gear
  • +
  • Sega Master System
  • +
  • Sega Genesis/Megadrive
  • +
  • Sega Saturn
  • +
  • Super Nintendo Entertainment System (SNES)
  • +
  • Super Famicom
  • +
  • TurboGraphx-16/PC Engine
  • +
  • Virtual Boy
  • +
  • WonderSwan
  • +
  • WonderSwan Color
  • +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Platforms-and-Players/MS-DOS/index.html b/4.4.0/Platforms-and-Players/MS-DOS/index.html new file mode 100644 index 0000000..74e824a --- /dev/null +++ b/4.4.0/Platforms-and-Players/MS-DOS/index.html @@ -0,0 +1,2850 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MS-DOS - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

MS-DOS

+ +

DOS is now supported in versions of RomM 4.0 and above thanks to the EmulatorJS player - Simply create a DOS platform to enable the integration

+ +
+

Info

+

I highly suggest you upload the games as .zip as the core can take advantage of unzipping and auto mounting options which are explained more below.

+
+ +
+

Info

+

Loading and saving states ARE supported so it's possible you only need to do the below steps once to load the game.

+
+

Running Games

+

Once you have the play button in the platform available for you there is some additional work you need to do in order to get the games playable and running. You first need to identify what DOS game you are trying to run, there would be three categories

+
    +
  • Homebrew
      +
    • Made by an indie dev, usually just an .exe file which will work fine in DOS once mounted.
    • +
    +
  • +
  • Demo
      +
    • Most sites which offer DOS games are shareware demo versions, these function similiar to homebrew and will have all the files needed within the folder.
    • +
    +
  • +
  • Retail
      +
    • These will need the CD mounted alongside the game files in order to play the games, this can get tricky and will be a unique per game basis but once you have cracked it you won't need to modify it again.
    • +
    +
  • +
+

The official method to run the games from the EmulatorJS dev is the following (Only works for Homebrew and Demos):

+
    +
  • Select commandline from the initial loading screen
  • +
  • mount A / -t floppy - This will mount the location of the files
  • +
  • A: - This will take you to the location of the files
  • +
  • dir - to find the .EXE file
  • +
  • filename.exe - This will run the .exe and run the game, you might need some additional configuration but that is purely on the dosbox side and you might need to run the setup.exe file or a file name similiar.
  • +
+

Advanced Running Games

+ +
+

Warning

+

This is not for the faint of heart and will require a lot of trial and error.

+
+

As the system is using DOSBOX pure it has a neat trick where it will run .CONF files it finds and automatically mount locations and run .exe files automatically, at the minute this is highly experimental and might be more effort then it is worth, but if you want the files running perfectly then I would suggest you look into this method, but it is extremely trail and error.

+

When you run a game in DOSBOX Pure, before it runs and mounts anything it will look for a .conf file and follow those instructions, this way we can actually auto mount locations, mount the required CDs and play the game without typing anything, you just click play, the auto mount does everything in the background and you are presented with the game.

+

This is an example using the doom shareware file which has all the files in the folder.

+
    +
  • Check the folder and make sure there is an .exe file for the game, make sure there is no *.ins, *.cue or *.bin files, if these exist it usually means this is a CD required game and these instructions will not work
  • +
  • Create a new text document named the same as the .exe (DOOM.conf) and add the following information:
  • +
+
+ +DOOM.conf Example + +
# This is the configurationfile for DOSBox 0.74. (Please use the latest version of DOSBox)
+# Lines starting with a # are commentlines and are ignored by DOSBox.
+# They are used to (briefly) document the effect of each option.
+
+[sdl]
+#       fullscreen: Start dosbox directly in fullscreen. (Press ALT-Enter to go back)
+#       fulldouble: Use double buffering in fullscreen. It can reduce screen flickering, but it can also result in a slow DOSBox.
+#   fullresolution: What resolution to use for fullscreen: original or fixed size (e.g. 1024x768).
+#                     Using your monitor's native resolution with aspect=true might give the best results.
+#                     If you end up with small window on a large screen, try an output different from surface.
+# windowresolution: Scale the window to this size IF the output device supports hardware scaling.
+#                     (output=surface does not!)
+#           output: What video system to use for output.
+#                   Possible values: surface, overlay, opengl, openglnb, ddraw.
+#         autolock: Mouse will automatically lock, if you click on the screen. (Press CTRL-F10 to unlock)
+#      sensitivity: Mouse sensitivity.
+#      waitonerror: Wait before closing the console if dosbox has an error.
+#         priority: Priority levels for dosbox. Second entry behind the comma is for when dosbox is not focused/minimized.
+#                     pause is only valid for the second entry.
+#                   Possible values: lowest, lower, normal, higher, highest, pause.
+#       mapperfile: File used to load/save the key/event mappings from. Resetmapper only works with the defaul value.
+#     usescancodes: Avoid usage of symkeys, might not work on all operating systems.
+
+fullscreen=TRUE
+fulldouble=false
+fullresolution=Fixed
+windowresolution=1280x800
+output=direct3d
+autolock=true
+sensitivity=100
+waitonerror=true
+priority=higher,normal
+mapperfile=mapper-0.74.map
+usescancodes=true
+
+[dosbox]
+# language: Select another language file.
+#  machine: The type of machine tries to emulate.
+#           Possible values: hercules, cga, tandy, pcjr, ega, vgaonly, svga_s3, svga_et3000, svga_et4000, svga_paradise, vesa_nolfb, vesa_oldvbe.
+# captures: Directory where things like wave, midi, screenshot get captured.
+#  memsize: Amount of memory DOSBox has in megabytes.
+#             This value is best left at its default to avoid problems with some games,
+#             though few games might require a higher value.
+#             There is generally no speed advantage when raising this value.
+
+language=
+machine=svga_s3
+captures=.\Captures\
+memsize=16
+
+[render]
+# frameskip: How many frames DOSBox skips before drawing one.
+#    aspect: Do aspect correction, if your output method doesn't support scaling this can slow things down!.
+#    scaler: Scaler used to enlarge/enhance low resolution modes.
+#              If 'forced' is appended, then the scaler will be used even if the result might not be desired.
+#            Possible values: none, normal2x, normal3x, advmame2x, advmame3x, advinterp2x, advinterp3x, hq2x, hq3x, 2xsai, super2xsai, supereagle, tv2x, tv3x, rgb2x, rgb3x, scan2x, scan3x.
+
+frameskip=0
+aspect=false
+scaler=normal3x
+
+[cpu]
+#      core: CPU Core used in emulation. auto will switch to dynamic if available and appropriate.
+#            Possible values: auto, dynamic, normal, simple.
+#   cputype: CPU Type used in emulation. auto is the fastest choice.
+#            Possible values: auto, 386, 386_slow, 486_slow, pentium_slow, 386_prefetch.
+#    cycles: Amount of instructions DOSBox tries to emulate each millisecond.
+#            Setting this value too high results in sound dropouts and lags.
+#            Cycles can be set in 3 ways:
+#              'auto'          tries to guess what a game needs.
+#                              It usually works, but can fail for certain games.
+#              'fixed #number' will set a fixed amount of cycles. This is what you usually need if 'auto' fails.
+#                              (Example: fixed 4000).
+#              'max'           will allocate as much cycles as your computer is able to handle.
+#
+#            Possible values: auto, fixed, max.
+#   cycleup: Amount of cycles to decrease/increase with keycombo.(CTRL-F11/CTRL-F12)
+# cycledown: Setting it lower than 100 will be a percentage.
+
+core=auto
+cputype=auto
+cycles=max
+cycleup=10
+cycledown=20
+
+[mixer]
+#   nosound: Enable silent mode, sound is still emulated though.
+#      rate: Mixer sample rate, setting any device's rate higher than this will probably lower their sound quality.
+#            Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
+# blocksize: Mixer block size, larger blocks might help sound stuttering but sound will also be more lagged.
+#            Possible values: 1024, 2048, 4096, 8192, 512, 256.
+# prebuffer: How many milliseconds of data to keep on top of the blocksize.
+
+nosound=false
+rate=22050
+blocksize=2048
+prebuffer=10
+
+[midi]
+#     mpu401: Type of MPU-401 to emulate.
+#             Possible values: intelligent, uart, none.
+# mididevice: Device that will receive the MIDI data from MPU-401.
+#             Possible values: default, win32, alsa, oss, coreaudio, coremidi, none.
+# midiconfig: Special configuration options for the device driver. This is usually the id of the device you want to use.
+#               See the README/Manual for more details.
+
+mpu401=intelligent
+mididevice=default
+midiconfig=
+
+[sblaster]
+#  sbtype: Type of Soundblaster to emulate. gb is Gameblaster.
+#          Possible values: sb1, sb2, sbpro1, sbpro2, sb16, gb, none.
+#  sbbase: The IO address of the soundblaster.
+#          Possible values: 220, 240, 260, 280, 2a0, 2c0, 2e0, 300.
+#     irq: The IRQ number of the soundblaster.
+#          Possible values: 7, 5, 3, 9, 10, 11, 12.
+#     dma: The DMA number of the soundblaster.
+#          Possible values: 1, 5, 0, 3, 6, 7.
+#    hdma: The High DMA number of the soundblaster.
+#          Possible values: 1, 5, 0, 3, 6, 7.
+# sbmixer: Allow the soundblaster mixer to modify the DOSBox mixer.
+# oplmode: Type of OPL emulation. On 'auto' the mode is determined by sblaster type. All OPL modes are Adlib-compatible, except for 'cms'.
+#          Possible values: auto, cms, opl2, dualopl2, opl3, none.
+#  oplemu: Provider for the OPL emulation. compat might provide better quality (see oplrate as well).
+#          Possible values: default, compat, fast.
+# oplrate: Sample rate of OPL music emulation. Use 49716 for highest quality (set the mixer rate accordingly).
+#          Possible values: 44100, 49716, 48000, 32000, 22050, 16000, 11025, 8000.
+
+sbtype=sb16
+sbbase=220
+irq=7
+dma=1
+hdma=5
+sbmixer=true
+oplmode=auto
+oplemu=default
+oplrate=44100
+
+[gus]
+#      gus: Enable the Gravis Ultrasound emulation.
+#  gusrate: Sample rate of Ultrasound emulation.
+#           Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
+#  gusbase: The IO base address of the Gravis Ultrasound.
+#           Possible values: 240, 220, 260, 280, 2a0, 2c0, 2e0, 300.
+#   gusirq: The IRQ number of the Gravis Ultrasound.
+#           Possible values: 5, 3, 7, 9, 10, 11, 12.
+#   gusdma: The DMA channel of the Gravis Ultrasound.
+#           Possible values: 3, 0, 1, 5, 6, 7.
+# ultradir: Path to Ultrasound directory. In this directory
+#           there should be a MIDI directory that contains
+#           the patch files for GUS playback. Patch sets used
+#           with Timidity should work fine.
+
+gus=false
+gusrate=44100
+gusbase=240
+gusirq=5
+gusdma=3
+ultradir=C:\ULTRASND
+
+[speaker]
+# pcspeaker: Enable PC-Speaker emulation.
+#    pcrate: Sample rate of the PC-Speaker sound generation.
+#            Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
+#     tandy: Enable Tandy Sound System emulation. For 'auto', emulation is present only if machine is set to 'tandy'.
+#            Possible values: auto, on, off.
+# tandyrate: Sample rate of the Tandy 3-Voice generation.
+#            Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
+#    disney: Enable Disney Sound Source emulation. (Covox Voice Master and Speech Thing compatible).
+
+pcspeaker=true
+pcrate=44100
+tandy=auto
+tandyrate=44100
+disney=true
+
+[joystick]
+# joysticktype: Type of joystick to emulate: auto (default), none,
+#               2axis (supports two joysticks),
+#               4axis (supports one joystick, first joystick used),
+#               4axis_2 (supports one joystick, second joystick used),
+#               fcs (Thrustmaster), ch (CH Flightstick).
+#               none disables joystick emulation.
+#               auto chooses emulation depending on real joystick(s).
+#               (Remember to reset dosbox's mapperfile if you saved it earlier)
+#               Possible values: auto, 2axis, 4axis, 4axis_2, fcs, ch, none.
+#        timed: enable timed intervals for axis. Experiment with this option, if your joystick drifts (away).
+#     autofire: continuously fires as long as you keep the button pressed.
+#       swap34: swap the 3rd and the 4th axis. can be useful for certain joysticks.
+#   buttonwrap: enable button wrapping at the number of emulated buttons.
+
+joysticktype=fcs
+timed=true
+autofire=false
+swap34=false
+buttonwrap=false
+
+[serial]
+# serial1: set type of device connected to com port.
+#          Can be disabled, dummy, modem, nullmodem, directserial.
+#          Additional parameters must be in the same line in the form of
+#          parameter:value. Parameter for all types is irq (optional).
+#          for directserial: realport (required), rxdelay (optional).
+#                           (realport:COM1 realport:ttyS0).
+#          for modem: listenport (optional).
+#          for nullmodem: server, rxdelay, txdelay, telnet, usedtr,
+#                         transparent, port, inhsocket (all optional).
+#          Example: serial1=modem listenport:5000
+#          Possible values: dummy, disabled, modem, nullmodem, directserial.
+# serial2: see serial1
+#          Possible values: dummy, disabled, modem, nullmodem, directserial.
+# serial3: see serial1
+#          Possible values: dummy, disabled, modem, nullmodem, directserial.
+# serial4: see serial1
+#          Possible values: dummy, disabled, modem, nullmodem, directserial.
+
+serial1=dummy
+serial2=dummy
+serial3=disabled
+serial4=disabled
+
+[dos]
+#            xms: Enable XMS support.
+#            ems: Enable EMS support.
+#            umb: Enable UMB support.
+# keyboardlayout: Language code of the keyboard layout (or none).
+
+xms=true
+ems=true
+umb=true
+keyboardlayout=auto
+
+[ipx]
+# ipx: Enable ipx over UDP/IP emulation.
+
+ipx=false
+
+[autoexec]
+# Lines in this section will be run at startup.
+# You can put your MOUNT lines here.
+
+@echo off
+Mount C ".."
+C:
+cls
+DOOM.exe
+:exit
+exit
+
+ +
+ +
    +
  • Most of the file is explained but to go into the nitty gritty of the [autoexec] it will mount the local location as C:, it changes to C: it will clear the screen and automatically run the DOOM.exe executable, and will exit the previous shell, meaning it will run the game by just clicking play.
  • +
  • Zip up the folder and add it to RomM's DOS platform, then just simply click play. If you hit a blank screen it means that something is wrong with the auto exec, and will need manual troubleshooting.
  • +
+

Advanced Running Retail Games

+ +
+

Info

+

At the minute DOS games redesigned by GOG are NOT supported, this is due to how they mount and use the locations. I am looking into how I can figure this out but I have had a 100% failure rate from the GOG DOS Games.

+
+

Retail games usually require to run alongside a disk even if the game has been "installed" locally. I will use Dungeon Keeper Gold as an example for a retail game with a disc and the configuration needed.

+
    +
  • Prepare the files like you did before, but this time with the disk images (*.bin & *.cue) and move them to a folder named CD within the folder. Only within this folder you should all the .bin files and the "matching" .cue sheet (remember .cue is just a text document pulling all these files together, like a playlist)
  • +
  • Have a look at the KEEPER.cfg file, this file will say how the game was installed and the path it expects the installed files to be at.
  • +
  • Create a new .conf document named the same as the .exe for me that would be KEEPER.conf
  • +
+
+ +KEEPER.conf Example + +
# This is the configurationfile for DOSBox 0.74. (Please use the latest version of DOSBox)
+# Lines starting with a # are commentlines and are ignored by DOSBox.
+# They are used to (briefly) document the effect of each option.
+
+[sdl]
+#       fullscreen: Start dosbox directly in fullscreen. (Press ALT-Enter to go back)
+#       fulldouble: Use double buffering in fullscreen. It can reduce screen flickering, but it can also result in a slow DOSBox.
+#   fullresolution: What resolution to use for fullscreen: original or fixed size (e.g. 1024x768).
+#                     Using your monitor's native resolution with aspect=true might give the best results.
+#                     If you end up with small window on a large screen, try an output different from surface.
+# windowresolution: Scale the window to this size IF the output device supports hardware scaling.
+#                     (output=surface does not!)
+#           output: What video system to use for output.
+#                   Possible values: surface, overlay, opengl, openglnb, ddraw.
+#         autolock: Mouse will automatically lock, if you click on the screen. (Press CTRL-F10 to unlock)
+#      sensitivity: Mouse sensitivity.
+#      waitonerror: Wait before closing the console if dosbox has an error.
+#         priority: Priority levels for dosbox. Second entry behind the comma is for when dosbox is not focused/minimized.
+#                     pause is only valid for the second entry.
+#                   Possible values: lowest, lower, normal, higher, highest, pause.
+#       mapperfile: File used to load/save the key/event mappings from. Resetmapper only works with the defaul value.
+#     usescancodes: Avoid usage of symkeys, might not work on all operating systems.
+
+fullscreen=TRUE
+fulldouble=false
+fullresolution=Fixed
+windowresolution=1280x800
+output=direct3d
+autolock=true
+sensitivity=100
+waitonerror=true
+priority=higher,normal
+mapperfile=mapper-0.74.map
+usescancodes=true
+
+[dosbox]
+# language: Select another language file.
+#  machine: The type of machine tries to emulate.
+#           Possible values: hercules, cga, tandy, pcjr, ega, vgaonly, svga_s3, svga_et3000, svga_et4000, svga_paradise, vesa_nolfb, vesa_oldvbe.
+# captures: Directory where things like wave, midi, screenshot get captured.
+#  memsize: Amount of memory DOSBox has in megabytes.
+#             This value is best left at its default to avoid problems with some games,
+#             though few games might require a higher value.
+#             There is generally no speed advantage when raising this value.
+
+language=
+machine=svga_s3
+captures=.\Captures\
+memsize=16
+
+[render]
+# frameskip: How many frames DOSBox skips before drawing one.
+#    aspect: Do aspect correction, if your output method doesn't support scaling this can slow things down!.
+#    scaler: Scaler used to enlarge/enhance low resolution modes.
+#              If 'forced' is appended, then the scaler will be used even if the result might not be desired.
+#            Possible values: none, normal2x, normal3x, advmame2x, advmame3x, advinterp2x, advinterp3x, hq2x, hq3x, 2xsai, super2xsai, supereagle, tv2x, tv3x, rgb2x, rgb3x, scan2x, scan3x.
+
+frameskip=0
+aspect=false
+scaler=normal3x
+
+[cpu]
+#      core: CPU Core used in emulation. auto will switch to dynamic if available and appropriate.
+#            Possible values: auto, dynamic, normal, simple.
+#   cputype: CPU Type used in emulation. auto is the fastest choice.
+#            Possible values: auto, 386, 386_slow, 486_slow, pentium_slow, 386_prefetch.
+#    cycles: Amount of instructions DOSBox tries to emulate each millisecond.
+#            Setting this value too high results in sound dropouts and lags.
+#            Cycles can be set in 3 ways:
+#              'auto'          tries to guess what a game needs.
+#                              It usually works, but can fail for certain games.
+#              'fixed #number' will set a fixed amount of cycles. This is what you usually need if 'auto' fails.
+#                              (Example: fixed 4000).
+#              'max'           will allocate as much cycles as your computer is able to handle.
+#
+#            Possible values: auto, fixed, max.
+#   cycleup: Amount of cycles to decrease/increase with keycombo.(CTRL-F11/CTRL-F12)
+# cycledown: Setting it lower than 100 will be a percentage.
+
+core=auto
+cputype=auto
+cycles=max
+cycleup=10
+cycledown=20
+
+[mixer]
+#   nosound: Enable silent mode, sound is still emulated though.
+#      rate: Mixer sample rate, setting any device's rate higher than this will probably lower their sound quality.
+#            Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
+# blocksize: Mixer block size, larger blocks might help sound stuttering but sound will also be more lagged.
+#            Possible values: 1024, 2048, 4096, 8192, 512, 256.
+# prebuffer: How many milliseconds of data to keep on top of the blocksize.
+
+nosound=false
+rate=22050
+blocksize=2048
+prebuffer=10
+
+[midi]
+#     mpu401: Type of MPU-401 to emulate.
+#             Possible values: intelligent, uart, none.
+# mididevice: Device that will receive the MIDI data from MPU-401.
+#             Possible values: default, win32, alsa, oss, coreaudio, coremidi, none.
+# midiconfig: Special configuration options for the device driver. This is usually the id of the device you want to use.
+#               See the README/Manual for more details.
+
+mpu401=intelligent
+mididevice=default
+midiconfig=
+
+[sblaster]
+#  sbtype: Type of Soundblaster to emulate. gb is Gameblaster.
+#          Possible values: sb1, sb2, sbpro1, sbpro2, sb16, gb, none.
+#  sbbase: The IO address of the soundblaster.
+#          Possible values: 220, 240, 260, 280, 2a0, 2c0, 2e0, 300.
+#     irq: The IRQ number of the soundblaster.
+#          Possible values: 7, 5, 3, 9, 10, 11, 12.
+#     dma: The DMA number of the soundblaster.
+#          Possible values: 1, 5, 0, 3, 6, 7.
+#    hdma: The High DMA number of the soundblaster.
+#          Possible values: 1, 5, 0, 3, 6, 7.
+# sbmixer: Allow the soundblaster mixer to modify the DOSBox mixer.
+# oplmode: Type of OPL emulation. On 'auto' the mode is determined by sblaster type. All OPL modes are Adlib-compatible, except for 'cms'.
+#          Possible values: auto, cms, opl2, dualopl2, opl3, none.
+#  oplemu: Provider for the OPL emulation. compat might provide better quality (see oplrate as well).
+#          Possible values: default, compat, fast.
+# oplrate: Sample rate of OPL music emulation. Use 49716 for highest quality (set the mixer rate accordingly).
+#          Possible values: 44100, 49716, 48000, 32000, 22050, 16000, 11025, 8000.
+
+sbtype=sb16
+sbbase=220
+irq=7
+dma=1
+hdma=5
+sbmixer=true
+oplmode=auto
+oplemu=default
+oplrate=44100
+
+[gus]
+#      gus: Enable the Gravis Ultrasound emulation.
+#  gusrate: Sample rate of Ultrasound emulation.
+#           Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
+#  gusbase: The IO base address of the Gravis Ultrasound.
+#           Possible values: 240, 220, 260, 280, 2a0, 2c0, 2e0, 300.
+#   gusirq: The IRQ number of the Gravis Ultrasound.
+#           Possible values: 5, 3, 7, 9, 10, 11, 12.
+#   gusdma: The DMA channel of the Gravis Ultrasound.
+#           Possible values: 3, 0, 1, 5, 6, 7.
+# ultradir: Path to Ultrasound directory. In this directory
+#           there should be a MIDI directory that contains
+#           the patch files for GUS playback. Patch sets used
+#           with Timidity should work fine.
+
+gus=false
+gusrate=44100
+gusbase=240
+gusirq=5
+gusdma=3
+ultradir=C:\ULTRASND
+
+[speaker]
+# pcspeaker: Enable PC-Speaker emulation.
+#    pcrate: Sample rate of the PC-Speaker sound generation.
+#            Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
+#     tandy: Enable Tandy Sound System emulation. For 'auto', emulation is present only if machine is set to 'tandy'.
+#            Possible values: auto, on, off.
+# tandyrate: Sample rate of the Tandy 3-Voice generation.
+#            Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
+#    disney: Enable Disney Sound Source emulation. (Covox Voice Master and Speech Thing compatible).
+
+pcspeaker=true
+pcrate=44100
+tandy=auto
+tandyrate=44100
+disney=true
+
+[joystick]
+# joysticktype: Type of joystick to emulate: auto (default), none,
+#               2axis (supports two joysticks),
+#               4axis (supports one joystick, first joystick used),
+#               4axis_2 (supports one joystick, second joystick used),
+#               fcs (Thrustmaster), ch (CH Flightstick).
+#               none disables joystick emulation.
+#               auto chooses emulation depending on real joystick(s).
+#               (Remember to reset dosbox's mapperfile if you saved it earlier)
+#               Possible values: auto, 2axis, 4axis, 4axis_2, fcs, ch, none.
+#        timed: enable timed intervals for axis. Experiment with this option, if your joystick drifts (away).
+#     autofire: continuously fires as long as you keep the button pressed.
+#       swap34: swap the 3rd and the 4th axis. can be useful for certain joysticks.
+#   buttonwrap: enable button wrapping at the number of emulated buttons.
+
+joysticktype=fcs
+timed=true
+autofire=false
+swap34=false
+buttonwrap=false
+
+[serial]
+# serial1: set type of device connected to com port.
+#          Can be disabled, dummy, modem, nullmodem, directserial.
+#          Additional parameters must be in the same line in the form of
+#          parameter:value. Parameter for all types is irq (optional).
+#          for directserial: realport (required), rxdelay (optional).
+#                           (realport:COM1 realport:ttyS0).
+#          for modem: listenport (optional).
+#          for nullmodem: server, rxdelay, txdelay, telnet, usedtr,
+#                         transparent, port, inhsocket (all optional).
+#          Example: serial1=modem listenport:5000
+#          Possible values: dummy, disabled, modem, nullmodem, directserial.
+# serial2: see serial1
+#          Possible values: dummy, disabled, modem, nullmodem, directserial.
+# serial3: see serial1
+#          Possible values: dummy, disabled, modem, nullmodem, directserial.
+# serial4: see serial1
+#          Possible values: dummy, disabled, modem, nullmodem, directserial.
+
+serial1=dummy
+serial2=dummy
+serial3=disabled
+serial4=disabled
+
+[dos]
+#            xms: Enable XMS support.
+#            ems: Enable EMS support.
+#            umb: Enable UMB support.
+# keyboardlayout: Language code of the keyboard layout (or none).
+
+xms=true
+ems=true
+umb=true
+keyboardlayout=auto
+
+[ipx]
+# ipx: Enable ipx over UDP/IP emulation.
+
+ipx=false
+
+[autoexec]
+# Lines in this section will be run at startup.
+# You can put your MOUNT lines here.
+
+@echo off
+Mount C ".."
+C:
+cd CD
+imgmount d DUNGEO~8.CUE -t iso -fs iso
+cd ..
+cls
+KEEPER.exe
+:exit
+exit
+
+ +
+ +
    +
  • Notice the different in the automount commands
      +
    • Mount the games files to can
    • +
    • Change to C:
    • +
    • Change Directory to CD
    • +
    • Using dosbox imgmount, mount the CD to the D path
    • +
    • cd to the directory above where the .exe
    • +
    • Clear the working out
    • +
    • Run KEEPER.exe
    • +
    +
  • +
  • Once this is done, the game will run. Zip the files up and add to your RomM and give it a test.
  • +
+

Advanced Game Troubleshooting

+

If the games are not working and you go back to a blank dos looking screen, this will require some manual troubleshooting but I have found success with the following methods:

+
    +
  • Remove the .exe in the .CONF file and investigate the automounts
      +
    • This means going to the C: and make sure the .exe is in the right place.
    • +
    • Use the following command type DEFAULT.cfg to see the installation location, this is where the game is looking for the files, this should be set to a D: location.
    • +
    • Browse to that location and see if the files are there and they are readable.
    • +
    +
  • +
  • The automount is simply following instructions, so it expects the files to be where you say, following this method is usually best to troubleshoot.
  • +
+

Another way of troubleshooting is to use Retroarch and the core "dosbox-pure" because if it runs in here, it will run in RomM. Simply give it your ZIP and it will act the same as if you was doing it through RomM, once the .conf file is perfected add it back to the ZIP.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Platforms-and-Players/RuffleRS-Player/index.html b/4.4.0/Platforms-and-Players/RuffleRS-Player/index.html new file mode 100644 index 0000000..6560685 --- /dev/null +++ b/4.4.0/Platforms-and-Players/RuffleRS-Player/index.html @@ -0,0 +1,2124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RuffleRS - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

RuffleRS

+ +

Ruffle is a web-based player for flash games. With flash now discontinued, this is the best way to play your flash collection in the browser.

+ +
+

Important

+

Ruffle will only play games stored in platform folders called flash or browser.

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Platforms-and-Players/Supported-Platforms/index.html b/4.4.0/Platforms-and-Players/Supported-Platforms/index.html new file mode 100644 index 0000000..eecdf76 --- /dev/null +++ b/4.4.0/Platforms-and-Players/Supported-Platforms/index.html @@ -0,0 +1,4599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Supported Platforms - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Supported Platforms

+ +

Below is a list of all supported platforms/systems/consoles and their respective folder names. Supported platforms means RomM can fetch metadata from sources for those platforms.

+ +
+

Info

+

For platforms that can be playable in the browser, please check EmulatorJS supported platforms and RuffleRS player.

+
+ +
+

Danger

+

The folder name is case-sensitive and must be used exactly as it appears in the list below.

+
+
+

Filter providers

+
+
+ + + + + + + + +
+
+ 0 of 0 platforms shown +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Platform NameFolder NameMetadata Providers
1292 Advanced Programmable Video System1292-advanced-programmable-video-systemigdb logo mobygames logo
3DO Interactive Multiplayer3doigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
8-Bit Productions Commander X16commander-x16hasheous logo
Aamber Pegasuspegasuslaunchbox logo
ABC 80abc-80mobygames logo
Acorn Archimedesacorn-archimedesigdb logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Acorn Electronacorn-electronigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo
Advanced Pico Beenaadvanced-pico-beenaigdb logo
Adventure Visionadventure-visionscreenscraper logo mobygames logo launchbox logo hasheous logo
AirConsoleairconsoleigdb logo mobygames logo
Alice 32/90alice-3290mobygames logo launchbox logo
Altair 680altair-680mobygames logo
Altair 8800altair-8800mobygames logo hasheous logo
Amazon Alexaamazon-alexamobygames logo
Amazon Fire TVamazon-fire-tvigdb logo mobygames logo
Amigaamigaigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Amiga CDamiga-cdscreenscraper logo
Amiga CD32amiga-cd32igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Amstrad CPCacpcigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Amstrad GX4000amstrad-gx4000igdb logo screenscraper logo launchbox logo hasheous logo
Amstrad PCWamstrad-pcwigdb logo mobygames logo hasheous logo
Analogue electronicsanalogueelectronicsigdb logo
Androidandroidigdb logo screenscraper logo mobygames logo launchbox logo
Antstreamantstreammobygames logo
APF MP1000/Imagination Machineapfmobygames logo launchbox logo hasheous logo
Apogee BK-01bk-01launchbox logo
Apple Iapplemobygames logo hasheous logo
Apple IIappleiiigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Apple IIGSapple-iigsigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo
Apple IIIappleiiihasheous logo
Apple Lisaapple-lisahasheous logo
Apple Pippinapple-pippinigdb logo hasheous logo
Arcadearcadeigdb logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Arcadia 2001arcadia-2001igdb logo screenscraper logo mobygames logo launchbox logo retroachivements logo
Arduboyarduboyigdb logo screenscraper logo mobygames logo hasheous logo retroachivements logo
Astral 2000astral-2000mobygames logo
Atari 2600atari2600igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Atari 5200atari5200igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Atari 7800atari7800igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Atari 8-bitatari8bitigdb logo screenscraper logo mobygames logo hasheous logo howlongtobeat logo
Atari 800atari800launchbox logo
Atari Jaguarjaguarigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Atari Jaguar CDatari-jaguar-cdigdb logo launchbox logo retroachivements logo howlongtobeat logo
Atari Lynxlynxigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Atari ST/STEatari-stigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Atari VCSatari-vcsmobygames logo
Atari XEGSatari-xegslaunchbox logo
Atomatomscreenscraper logo mobygames logo launchbox logo hasheous logo
AY-3-8500ay-3-8500igdb logo
AY-3-8603ay-3-8603igdb logo
AY-3-8605ay-3-8605igdb logo
AY-3-8606ay-3-8606igdb logo
AY-3-8607ay-3-8607igdb logo
AY-3-8610ay-3-8610igdb logo
AY-3-8710ay-3-8710igdb logo
AY-3-8760ay-3-8760igdb logo
Badabadamobygames logo
Bally Astrocadeastrocadeigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo
BBC Microcomputer Systembbcmicroigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Benesse Pocket Challenge V2pocket-challenge-v2hasheous logo
Benesse Pocket Challenge Wpocket-challenge-whasheous logo
BeOSbeosmobygames logo
BGR Computers Excalibur 64excalibur-64hasheous logo
Bit Corporation BIT 90bit-90hasheous logo
Black Pointblack-point
BlackBerry OSblackberryigdb logo mobygames logo
Blacknutblacknutmobygames logo
Blu-ray Playerblu-ray-playerigdb logo mobygames logo
BREWbrewmobygames logo
Browser (Flash/HTML5)browserigdb logo mobygames logo launchbox logo flashpoint logo howlongtobeat logo
Bubblebubblemobygames logo
Call-A-Computer time-shared mainframe computer systemcall-a-computerigdb logo
Cambridge Computer Z88z88hasheous logo
Camputers Lynxcamputers-lynxscreenscraper logo mobygames logo launchbox logo hasheous logo
Casio CFX-9850casio-cfx-9850hasheous logo
Casio FP-1000 & FP-1100casio-fp-1000hasheous logo
Casio Loopycasio-loopyigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo
Casio PB-1000casio-pb-1000hasheous logo
Casio Programmable Calculatorcasio-programmable-calculatormobygames logo
Casio PV-1000casio-pv-1000screenscraper logo mobygames logo launchbox logo hasheous logo
Casio PV-2000casio-pv-2000hasheous logo
CDC Cyber 70cdccyber70igdb logo
Champion 2711champion-2711mobygames logo
ClickStartclickstartmobygames logo
Coleco Adamcolecoadamscreenscraper logo mobygames logo launchbox logo
ColecoVisioncolecovisionigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Colour Geniecolour-geniescreenscraper logo mobygames logo launchbox logo
Commodore 128c128screenscraper logo mobygames logo launchbox logo hasheous logo
Commodore 16c16igdb logo screenscraper logo mobygames logo hasheous logo
Commodore C64/128/MAXc64igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Commodore CDTVcommodore-cdtvigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo
Commodore PETcpetigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Commodore Plus/4c-plus-4igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Commodore VIC-20vic-20igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo
Compal 80compal-80mobygames logo
Compucolor Icompucolor-imobygames logo
Compucolor IIcompucolor-iimobygames logo
Compucorp Programmable Calculatorcompucorp-programmable-calculatormobygames logo
COSMACfred-cosmacmobygames logo
CP/Mcpmmobygames logo
CreatiVisioncreativisionscreenscraper logo mobygames logo launchbox logo
Cybervisioncybervisionmobygames logo
Danger OSdanger-osmobygames logo
Daydreamdaydreamigdb logo
DEC GT40gt40igdb logo
Dedicated consolededicated-consolemobygames logo
Dedicated handhelddedicated-handheldmobygames logo
Didjdidjmobygames logo
Digiblastdigiblastigdb logo mobygames logo
DoJadojamobygames logo
Donner Model 30donner30igdb logo
DOSdosigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo
Dragon 32/64dragon-32-slash-64igdb logo screenscraper logo mobygames logo launchbox logo
Dreamcastdcigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
DVD Playerdvd-playerigdb logo mobygames logo howlongtobeat logo
e-Reader / Card-e Readere-reader-slash-card-e-readerigdb logo
ECD Micromindecd-micromindmobygames logo
EDSACedsacigdb logo
Elektor TV Games Computerelektorigdb logo retroachivements logo
Elektronika BKbklaunchbox logo
Enterpriseenterprisemobygames logo launchbox logo
Epoch Cassette Visionepoch-cassette-visionigdb logo mobygames logo
Epoch Game Pocket Computerepoch-game-pocket-computerscreenscraper logo mobygames logo launchbox logo
Epoch Super Cassette Visionepoch-super-cassette-visionigdb logo screenscraper logo mobygames logo launchbox logo
Evercadeevercadeigdb logo mobygames logo howlongtobeat logo
Exelvisionexelvisionscreenscraper logo mobygames logo launchbox logo
ExEnexenmobygames logo
Exidy Sorcererexidy-sorcererigdb logo screenscraper logo mobygames logo launchbox logo
Fairchild Channel Ffairchild-channel-figdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo
Family Computerfamicomigdb logo mobygames logo launchbox logo retroachivements logo
Family Computer Disk Systemfdsigdb logo screenscraper logo launchbox logo hasheous logo
Feature phonemobile-custommobygames logo
Ferranti Nimrod Computernimrodigdb logo
FM Townsfm-townsigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
FM-7fm-7igdb logo screenscraper logo mobygames logo launchbox logo howlongtobeat logo
Freeboxfreeboxmobygames logo
G-clusterg-clustermobygames logo
Galaksijagalaksijamobygames logo
Gamategamateigdb logo hasheous logo
Game & Watchg-and-wigdb logo screenscraper logo mobygames logo launchbox logo howlongtobeat logo
Game Boygbigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Game Boy Advancegbaigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Game Boy Colorgbcigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Game Masterhartungscreenscraper logo launchbox logo
Game Wavegame-wavemobygames logo launchbox logo
Game.comgame-dot-comigdb logo screenscraper logo mobygames logo launchbox logo howlongtobeat logo
GameStickgamestickmobygames logo
Gear VRgear-vrigdb logo howlongtobeat logo
GIMINIgiminimobygames logo
Gizmondogizmondoigdb logo mobygames logo howlongtobeat logo
Gloudgloudmobygames logo
Glulxglulxmobygames logo
GNEXgnexmobygames logo
Google Stadiastadiaigdb logo mobygames logo howlongtobeat logo
GP2Xgp2xmobygames logo
GP2X Wizgp2x-wizmobygames logo
GP32gp32screenscraper logo mobygames logo launchbox logo
GVMgvmmobygames logo
Handheld Electronic LCDhandheld-electronic-lcdigdb logo
HD DVD Playerhd-dvd-playermobygames logo
Heath/Zenith H8/H89heathzenithmobygames logo
Heathkit H11heathkit-h11mobygames logo
Hector HRXhrxlaunchbox logo
Hitachi S1hitachi-s1mobygames logo
HP 2100hp2100igdb logo
HP 3000hp3000igdb logo
HP 9800hp-9800mobygames logo
HP Programmable Calculatorhp-programmable-calculatormobygames logo
Hugohugomobygames logo
Hyper Neo Geo 64hyper-neo-geo-64igdb logo
HyperScanhyperscanigdb logo mobygames logo launchbox logo
IBM 5100ibm-5100mobygames logo
IBM PCjrpc-jrhasheous logo
Ideal-Computerideal-computermobygames logo
iiRcadeiircademobygames logo
Imlac PDS-1imlac-pds1igdb logo
Intel 8008intel-8008mobygames logo
Intel 8080intel-8080mobygames logo
Intel 8086 / 8088intel-8086mobygames logo
Intellivisionintellivisionigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Intellivision Amicointellivision-amicoigdb logo
Interact Model Oneinteract-model-onemobygames logo
Interton VC 4000interton-vc-4000retroachivements logo
Interton Video 2000interton-video-2000mobygames logo
iOSiosigdb logo mobygames logo launchbox logo
iPadipadmobygames logo
iPod Classicipod-classicmobygames logo
J2MEj2memobygames logo
Joltjoltmobygames logo
Jupiter Acejupiter-acescreenscraper logo mobygames logo launchbox logo
KaiOSkaiosmobygames logo
KIM-1kim-1mobygames logo
Kindle Classickindlemobygames logo
Laser 200laser200mobygames logo
LaserActivelaseractiveigdb logo mobygames logo
LeapFrog Explorerleapfrog-explorermobygames logo
Leapsterleapsterigdb logo mobygames logo
Leapster Explorer/LeadPad Explorerleapster-explorer-slash-leadpad-explorerigdb logo mobygames logo
LeapTVleaptvigdb logo mobygames logo
Legacy Computerlegacy-computerigdb logo
Legacy Mobile Devicemobileigdb logo howlongtobeat logo
Linuxlinuxigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Lunalunamobygames logo howlongtobeat logo
Macmacigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Maemomaemomobygames logo
Magnavox Odysseyodysseyigdb logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Mainframemainframemobygames logo
Matsushita/Panasonic JRmatsushitapanasonic-jrmobygames logo
Mattel Aquariusaquariusmobygames logo launchbox logo hasheous logo
MeeGomeegomobygames logo
Mega Duck/Cougar Boymega-duck-slash-cougar-boyigdb logo launchbox logo retroachivements logo
Memotech MTXmemotech-mtxmobygames logo
Memotech MTX512mtx512launchbox logo
Meritummeritummobygames logo
Meta Quest 2meta-quest-2igdb logo
Meta Quest 3meta-quest-3igdb logo
Microbeemicrobeemobygames logo hasheous logo
Microcomputermicrocomputerigdb logo
Microsoft MSX2+msx2pluslaunchbox logo
Microtan 65microtan-65mobygames logo
Microvisionmicrovisionigdb logo mobygames logo
Mophunmophunmobygames logo
MOS Technology 6502mos-technology-6502mobygames logo
Motorola 6800motorola-6800mobygames logo
Motorola 68kmotorola-68kmobygames logo
MREmremobygames logo
MSXmsxigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
MSX Turbo Rmsx-turboscreenscraper logo
MSX2msx2igdb logo launchbox logo hasheous logo
MUGENmugenlaunchbox logo
N-Gagengageigdb logo screenscraper logo mobygames logo launchbox logo howlongtobeat logo
N-Gage (service)ngage2mobygames logo
Namco System 22system-32launchbox logo
Nascomnascommobygames logo
NEC PC-6000 Seriesnec-pc-6000-seriesigdb logo hasheous logo
Neo Geo AESneogeoaesigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Neo Geo CDneo-geo-cdigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Neo Geo MVSneogeomvsigdb logo screenscraper logo mobygames logo launchbox logo
Neo Geo Pocketneo-geo-pocketigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Neo Geo Pocket Colorneo-geo-pocket-colorigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo
Neo Geo Xneo-geo-xmobygames logo
New Nintendo 3DSnew-nintendo-3dsigdb logo mobygames logo hasheous logo
NewBrainnewbrainmobygames logo
Newtonnewtonmobygames logo
Nintendo 3DS3dsigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Nintendo 64n64igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Nintendo 64DD64ddigdb logo screenscraper logo launchbox logo hasheous logo
Nintendo DSndsigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Nintendo DSinintendo-dsiigdb logo screenscraper logo mobygames logo hasheous logo retroachivements logo
Nintendo Entertainment Systemnesigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Nintendo GameCubengcigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Nintendo Switchswitchigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Nintendo Switch 2switch-2igdb logo mobygames logo launchbox logo howlongtobeat logo
North Starnorthstarmobygames logo
Noval 760noval-760mobygames logo
Nuonnuonigdb logo mobygames logo launchbox logo
Oculus Gooculus-goigdb logo mobygames logo howlongtobeat logo
Oculus Questoculus-questigdb logo mobygames logo howlongtobeat logo
Oculus Riftoculus-riftigdb logo
Oculus VRoculus-vrigdb logo
Odyssey 2odyssey-2igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Ohio Scientificohio-scientificmobygames logo
OnLive Game Systemonlive-game-systemigdb logo mobygames logo howlongtobeat logo
OOPartsoopartsigdb logo mobygames logo
OpenBORopenborlaunchbox logo
Oraooraomobygames logo
Oricoricscreenscraper logo mobygames logo
Oric Atmosatmoslaunchbox logo
OS/2os2mobygames logo
Othello Multivisionmultivisionlaunchbox logo hasheous logo
Ouyaouyaigdb logo mobygames logo launchbox logo howlongtobeat logo
Palm OSpalm-osigdb logo screenscraper logo mobygames logo
Palmtexpalmtex
Panasonic Junglepanasonic-jungleigdb logo
Panasonic M2panasonic-m2igdb logo
Pandorapandoramobygames logo
PC Booterpc-bootermobygames logo
PC Engine SuperGrafxsupergrafxigdb logo screenscraper logo mobygames logo launchbox logo
PC-50X Familypc-50x-familyigdb logo
PC-6001pc-6001mobygames logo
PC-8000pc-8000mobygames logo
PC-8800 Seriespc-8800-seriesigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
PC-9800 Seriespc-9800-seriesigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
PC-FXpc-fxigdb logo screenscraper logo mobygames logo launchbox logo retroachivements logo howlongtobeat logo
PDP-1pdp1igdb logo
PDP-10pdp10igdb logo
PDP-11pdp11igdb logo
PDP-7pdp-7igdb logo
PDP-8pdp-8igdb logo
Pebblepebblemobygames logo
Philips CD-iphilips-cd-iigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Philips VG 5000philips-vg-5000screenscraper logo mobygames logo launchbox logo
Photo CDphotocdmobygames logo
PICOpicoscreenscraper logo mobygames logo launchbox logo howlongtobeat logo
Pinballpinballlaunchbox logo
Pippinpippinmobygames logo
PLATOplatoigdb logo
Playdateplaydateigdb logo mobygames logo howlongtobeat logo
Playdiaplaydiaigdb logo mobygames logo
PlayStationpsxigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
PlayStation 2ps2igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
PlayStation 3ps3igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
PlayStation 4ps4igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
PlayStation 5ps5igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
PlayStation Nowplaystation-nowmobygames logo howlongtobeat logo
PlayStation Portablepspigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
PlayStation Vitapsvitaigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
PlayStation VRpsvrigdb logo howlongtobeat logo
PlayStation VR2psvr2igdb logo
Plex Arcadeplex-arcademobygames logo
Plug & Playplug-and-playigdb logo howlongtobeat logo
PocketStationpocketstationigdb logo launchbox logo hasheous logo
Pokittopokittomobygames logo
Pokémon minipokemon-miniigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo
Poly-88poly-88mobygames logo
Polymegapolymegaigdb logo
R-Zoner-zoneigdb logo
RCA Studio IIrca-studio-iimobygames logo launchbox logo hasheous logo
Research Machines 380Zresearch-machines-380zmobygames logo
Rokurokumobygames logo
SAM Coupésam-coupescreenscraper logo mobygames logo launchbox logo
Satellaviewsatellaviewigdb logo launchbox logo
SC/MPscmpmobygames logo
ScummVMscummvmlaunchbox logo
SD-200/270/290sd-200270290mobygames logo
SDS Sigma 7sdssigma7igdb logo
Sega 32Xsega32igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Sega Advanced Pico Beenabeenahasheous logo
Sega CDsegacdigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Sega CD 32Xsegacd32igdb logo launchbox logo
Sega Dreamcast VMUvmulaunchbox logo
Sega Game Geargamegearigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Sega Hikaruhikarulaunchbox logo
Sega Master System/Mark IIIsmsigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Sega Mega Drive/Genesisgenesisigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Sega Model 1model1launchbox logo
Sega Model 2model2launchbox logo
Sega Model 3model3launchbox logo
Sega Picosega-picoigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Sega Saturnsaturnigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Sega SC-3000sc3000launchbox logo hasheous logo
Sega ST-Vstvlaunchbox logo
Sega System 16system16launchbox logo
Sega System 32system32launchbox logo
SG-1000sg1000igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Sharp MZ-2200sharp-mz-2200igdb logo
Sharp MZ-80B/2000/2500sharp-mz-80b20002500mobygames logo launchbox logo
Sharp MZ-80K/700/800/1500sharp-mz-80k7008001500mobygames logo
Sharp X1x1igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Sharp X68000sharp-x68000igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Sharp Zaurussharp-zaurusmobygames logo
Signetics 2650signetics-2650mobygames logo
Sinclair QLsinclair-qligdb logo mobygames logo hasheous logo
Sinclair ZX81zx81igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
SK-VMsk-vmmobygames logo
SMC-777smc-777mobygames logo
Socratessocratesmobygames logo launchbox logo
Sol-20sol-20igdb logo mobygames logo
Sony PSP Minispsp-minislaunchbox logo
Sord M5sord-m5mobygames logo launchbox logo
Spectravideospectravideoscreenscraper logo mobygames logo launchbox logo
SRI-500/1000sri-5001000mobygames logo
SteamVRsteam-vrigdb logo
Sufami Turbosufami-turboscreenscraper logo
Super A'Cansuper-acanigdb logo screenscraper logo mobygames logo
Super Famicomsfamigdb logo mobygames logo launchbox logo hasheous logo retroachivements logo
Super NES CD-ROM Systemsuper-nes-cd-rom-systemigdb logo
Super Nintendo Entertainment Systemsnesigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Super Vision 8000super-vision-8000mobygames logo launchbox logo hasheous logo
Sure Shot HDsure-shot-hdmobygames logo
SwanCrystalswancrystaligdb logo
SWTPC 6800swtpc-6800mobygames logo
Symbiansymbianmobygames logo
TADStadsmobygames logo
Taito Type Xtype-xlaunchbox logo
Taito X-55taito-x-55screenscraper logo mobygames logo
Tandy Vistandy-vis
Tapwave Zodiaczodigdb logo launchbox logo
Tatung Einsteintatung-einsteinigdb logo mobygames logo
Tektronix 4050tektronix-4050mobygames logo
Tele-Spiel ES-2201tele-spielmobygames logo
Telstar Arcadetelstar-arcademobygames logo
Terebikko / See 'n Say Video Phoneterebikko-slash-see-n-say-video-phoneigdb logo
Terminalterminalmobygames logo
Texas Instruments TI-82ti-82hasheous logo
Texas Instruments TI-83ti-83hasheous logo
Texas Instruments TI-99ti-99igdb logo screenscraper logo mobygames logo
Thomson MO5thomson-mo5igdb logo screenscraper logo mobygames logo
Thomson TOthomson-toscreenscraper logo mobygames logo
TI Programmable Calculatorti-programmable-calculatormobygames logo
TI-99/4Ati-994amobygames logo launchbox logo
Tiki 100tiki-100mobygames logo
TIMtimmobygames logo
Timex Sinclair 2068timex-sinclair-2068mobygames logo
Tizentizenmobygames logo
Tomahawk F1tomahawk-f1mobygames logo
Tomy Tutortomy-tutormobygames logo launchbox logo
Tomy Tutor / Pyuta / Grandstand Tutortomy-tutor-slash-pyuta-slash-grandstand-tutorigdb logo
Tritontritonmobygames logo
TRS-80trs-80igdb logo mobygames logo launchbox logo hasheous logo
TRS-80 Color Computertrs-80-color-computerigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo
TRS-80 MC-10trs-80-mc-10mobygames logo
TRS-80 Model 100trs-80-model-100mobygames logo
TurboGrafx-16/PC Enginetg16igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Turbografx-16/PC Engine CDturbografx-cdigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
tvOStvosmobygames logo
Uzeboxuzeboxigdb logo screenscraper logo retroachivements logo
V.Flashvflashmobygames logo
V.Smilevsmileigdb logo screenscraper logo mobygames logo launchbox logo
VC 4000vc-4000igdb logo launchbox logo
Vector-06C06claunchbox logo
Vectrexvectrexigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Versatileversatilemobygames logo
VideoBrainvideobrainmobygames logo
Videopac+ G7400videopac-g7400screenscraper logo mobygames logo launchbox logo
Virtual Boyvirtualboyigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Virtual Consolevcigdb logo
VISvismobygames logo
visionOSvisionosigdb logo
Visual Memory Unit / Visual Memory Systemvisual-memory-unit-slash-visual-memory-systemigdb logo
Wang 2200wang2200mobygames logo
WASM-4wasm-4retroachivements logo
Watara/QuickShot Supervisionsupervisionigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo
watchOSwatchosmobygames logo
webOSwebosmobygames logo
Wiiwiiigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Wii Uwiiuigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Windowswinigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
Windows 3.xwin3xscreenscraper logo mobygames logo launchbox logo
Windows Appswindows-appsmobygames logo
Windows Mixed Realitywindows-mixed-realityigdb logo
Windows Mobilewindows-mobileigdb logo mobygames logo
Windows Phonewinphoneigdb logo mobygames logo howlongtobeat logo
WIPIwipimobygames logo
WonderSwanwonderswanigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo howlongtobeat logo
WonderSwan Colorwonderswan-colorigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo retroachivements logo
WoW Action Maxaction-maxlaunchbox logo hasheous logo
XaviXPORTxavixportmobygames logo launchbox logo
Xboxxboxigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Xbox 360xbox360igdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Xbox Cloud Gamingxboxcloudgamingmobygames logo
Xbox Onexboxoneigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Xbox Series X/Sseries-x-sigdb logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
Xerox Altoxerox-altomobygames logo
Z-machinez-machinescreenscraper logo mobygames logo
Zeebozeeboigdb logo mobygames logo howlongtobeat logo
Zilog Z80z80mobygames logo
Zilog Z8000zilog-z8000mobygames logo
ZiNczinclaunchbox logo
Zodiaczodiacmobygames logo
Zunezunemobygames logo
ZX Spectrumzxsigdb logo screenscraper logo mobygames logo launchbox logo hasheous logo howlongtobeat logo
ZX Spectrum Nextzx-spectrum-nextmobygames logo
ZX80zx80mobygames logo hasheous logo
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/System-Setup/Synology-Setup-Guide/index.html b/4.4.0/System-Setup/Synology-Setup-Guide/index.html new file mode 100644 index 0000000..6c42fa9 --- /dev/null +++ b/4.4.0/System-Setup/Synology-Setup-Guide/index.html @@ -0,0 +1,2628 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Synology Setup - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Synology Setup

+ +

External Written Guides

+

While you can follow the below guide, Marius Bogdan Lixandru has written excellent guides which focus on Synology infrastructure and with support for both MariaDB and PostGresSQL:

+ +

We suggest following the above guides if they fit your setup, and the guide below is available for all other use cases.

+

Prerequisites

+

This guide assumes you're familiar with Docker and have basic knowledge of server management. You'll need:

+
    +
  • A Synology NAS or similar server
  • +
  • Docker installed
  • +
  • Basic command line knowledge
  • +
  • Access to manage network settings
  • +
+

Setup Process

+

1. Folder Structure Setup

+

ROM Storage Folders

+

Create the following directory structure for game assets and configuration:

+
mkdir -p /volume1/data/media/games/assets
+mkdir -p /volume1/data/media/games/config
+
+

ROM Library Structure

+

RomM requires a very specific folder structure for rom files:

+
mkdir -p /volume1/data/media/games/library/roms
+mkdir -p /volume1/data/media/games/library/bios
+
+

Note: For supported platforms and their specific folder names, refer to the official RomM docs.

+

Docker Data Folders

+

Create these folders for project and container data:

+
mkdir -p /volume1/docker/romm-project/
+mkdir -p /volume1/docker/romm/resources
+mkdir -p /volume1/docker/romm/redis-data
+mkdir -p /volume1/docker/mariadb-romm
+
+

2. Network Bridge Setup

+

Create a new network bridge named rommbridge following standard Docker networking practices. You can use this guide for reference.

+

3. Key Generation

+

Authentication Key

+

Generate your authentication key using:

+
openssl rand -hex 32
+> 03a054b6ca27e0107c5eed552ea66bacd9f3a2a8a91e7595cd462a593f9ecd09
+
+

Save the output - you'll need it for the ROMM_AUTH_SECRET_KEY in your configuration.

+

API Integration Setup

+

Follow the dedicated docs page for API key generation to set up your API keys.

+

4. MariaDB Configuration

+ +
+

Important

+
    +
  • This guide uses a dedicated MariaDB container for RomM, but you can use an existing MariaDB instance if preferred
  • +
  • We're using MariaDB version 10.7 for compatibility
  • +
  • The container uses port 3306 internally, mapped to 3309 externally
  • +
  • A simplified health check is implemented for stability
  • +
+
+

5. Docker Compose Configuration

+

Create a docker-compose.yml file with the following content:

+ +
+Example Docker Compose +
version: "3"
+
+volumes:
+    mysql_data:
+
+services:
+    romm:
+        image: rommapp/romm:latest
+        container_name: romm
+        restart: unless-stopped
+        environment:
+            - DB_HOST=romm-db
+            - DB_NAME=romm # Should match MARIADB_DATABASE in mariadb
+            - DB_USER=romm-user # Should match MARIADB_USER in mariadb
+            - DB_PASSWD= # Should match MARIADB_PASSWORD in mariadb
+            - ROMM_AUTH_SECRET_KEY= # Generate a key with `openssl rand -hex 32`
+            - SCREENSCRAPER_USER= # These are the recommended metadata providers
+            - SCREENSCRAPER_PASSWORD= # https://docs.romm.app/latest/Getting-Started/Metadata-Providers/#screenscraper
+            - RETROACHIEVEMENTS_API_KEY= # https://docs.romm.app/latest/Getting-Started/Metadata-Providers/#retroachievements
+            - STEAMGRIDDB_API_KEY= # https://docs.romm.app/latest/Getting-Started/Metadata-Providers/#steamgriddb
+            - HASHEOUS_API_ENABLED=true # https://docs.romm.app/latest/Getting-Started/Metadata-Providers/#hasheous
+        volumes:
+            - /volume1/docker/romm/resources:/romm/resources
+            - /volume1/docker/romm/redis-data:/redis-data
+            - /volume1/data/media/games/library:/romm/library
+            - /volume1/data/media/games/assets:/romm/assets
+            - /volume1/data/media/games/config:/romm/config
+        ports:
+            - 7676:8080
+        network_mode: rommbridge
+        depends_on:
+            romm-db:
+                condition: service_healthy
+                restart: true
+
+    romm-db:
+        image: mariadb:latest
+        container_name: romm-db
+        restart: unless-stopped
+        environment:
+            - MARIADB_ROOT_PASSWORD= # Use a unique, secure password
+            - MARIADB_DATABASE=romm
+            - MARIADB_USER=romm-user
+            - MARIADB_PASSWORD=
+        ports:
+            - 3309:3306
+        network_mode: rommbridge
+        volumes:
+            - /volume1/docker/mariadb-romm:/var/lib/mysql
+        healthcheck:
+            test: [CMD, healthcheck.sh, --connect, --innodb_initialized]
+            start_period: 30s
+            start_interval: 10s
+            interval: 10s
+            timeout: 5s
+            retries: 5
+
+
+

6. Initial Launch

+
    +
  1. Start the containers using Docker Compose
  2. +
  3. Be patient! The container can take a few minutes to setup on first launch
  4. +
  5. Monitor progress through container logs
  6. +
  7. Access RomM through your browser at http://your-server-ip:7676
  8. +
+ +
+

Important

+
    +
  • Replace placeholder values (UIDs, GIDs, passwords, API keys) with your own
  • +
  • Ensure proper permissions on all created directories
  • +
  • Back up your configuration after successful setup
  • +
  • Monitor logs during initial startup for any errors
  • +
+
+

Troubleshooting

+
    +
  • If the web interface shows "page not found," wait for initial setup to complete
  • +
  • For database connection issues, verify MariaDB container health status
  • +
  • Check logs for both containers if experiencing issues
  • +
  • Ensure all volumes are properly mounted with correct permissions
  • +
+

Contributing

+

This guide is an abridged version of ChopFoo's original guide. If you have any suggestions or improvements, please submit a pull request to the RomM docs.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/System-Setup/TrueNAS-Setup-Guide/index.html b/4.4.0/System-Setup/TrueNAS-Setup-Guide/index.html new file mode 100644 index 0000000..496152c --- /dev/null +++ b/4.4.0/System-Setup/TrueNAS-Setup-Guide/index.html @@ -0,0 +1,2591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TrueNAS Setup - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

TrueNAS Setup

+ +

Prerequisites

+

This guide assumes you're familiar with Docker and have basic knowledge of TrueNAS. You'll need:

+ +

Setup Process

+ +

Step 1: Navigate to RomM app

+

Navigate to the App Catalog via Apps (Left navigation bar) -> Discover Apps -> RomM -> Install

+

RomM app

+

Step 2: Installation configuration

+

Step through the installation UI. You will need to supply various credentials per the Quick Start Guide. Most of the default values will work.

+

Note: You will likely want to set certain Storage Configurations to a Dataset within TrueNAS, such as your RomM Library and Assets storage. If you do this, ensure you provide ACL access to the UserID specified above (default: 568, apps user).

+

RomM Library Example

+

Step 3: Save your configuration

+

Save, and you're done! If the app will not boot, refer to Troubleshooting or head on over to the Discord.

+

Install via YAML

+

This installation path should only be used in the event that there is a bug with installing through the App Catalog, or you wish to have more flexibility than is provided by the installation UI.

+

Step 1: Navigate to YAML install

+

Navigate to the Install via YAML page via Apps (Left navigation bar) -> Discover Apps -> Install via YAML

+

Install via YAML

+

Step 2: Paste in the following YML

+

Replace any empty values with credentials you've created per the Quick Start Guide.

+ +
+Example Docker Compose +
version: "3"
+
+volumes:
+    mysql_data:
+    romm_redis_data:
+
+services:
+    romm:
+        image: rommapp/romm:latest
+        container_name: romm
+        restart: unless-stopped
+        user: 568:568
+        environment:
+            - DB_HOST=romm-db
+            - DB_NAME=romm # Should match MARIADB_DATABASE in mariadb
+            - DB_USER=romm-user # Should match MARIADB_USER in mariadb
+            - DB_PASSWD= # Should match MARIADB_PASSWORD in mariadb
+            - ROMM_AUTH_SECRET_KEY= # Generate a key with `openssl rand -hex 32`
+            - SCREENSCRAPER_USER= # These are the recommended metadata providers
+            - SCREENSCRAPER_PASSWORD= # https://docs.romm.app/latest/Getting-Started/Metadata-Providers/#screenscraper
+            - RETROACHIEVEMENTS_API_KEY= # https://docs.romm.app/latest/Getting-Started/Metadata-Providers/#retroachievements
+            - STEAMGRIDDB_API_KEY= # https://docs.romm.app/latest/Getting-Started/Metadata-Providers/#steamgriddb
+            - HASHEOUS_API_ENABLED=true # https://docs.romm.app/latest/Getting-Started/Metadata-Providers/#hasheous
+        volumes: # Any /mnt paths may optionally be replaced with a docker volume
+            - /mnt/tank/truenas/resources:/romm/resources # Replace /mnt...: file path with your own data structure
+            - romm_redis_data:/romm/redis-data # Docker will manage this volume
+            - /mnt/tank/truenas/roms:/romm/library # Replace /mnt...: file path with your own data structure
+            - /mnt/tank/truenas/assets:/romm/assets # Replace /mnt...: file path with your own data structure
+            - /mnt/tank/truenas/config:/romm/config # Replace /mnt...: file path with your own data structure
+        ports:
+            - 31100:8080
+        depends_on:
+            romm-db:
+                condition: service_healthy
+                restart: true
+        deploy:
+            resources:
+                limits:
+                    cpus: "2.0"
+                    memory: 4g
+
+    romm-db:
+        image: mariadb:latest
+        container_name: romm-db
+        restart: unless-stopped
+        environment:
+            - MARIADB_ROOT_PASSWORD= # Use a unique, secure password
+            - MARIADB_DATABASE=romm
+            - MARIADB_USER=romm-user
+            - MARIADB_PASSWORD=
+        volumes:
+            - mysql_data:/var/lib/mysql
+        healthcheck:
+            test: [CMD, healthcheck.sh, --connect, --innodb_initialized]
+            start_period: 30s
+            start_interval: 10s
+            interval: 10s
+            timeout: 5s
+            retries: 5
+
+
+

Step 3: Save the configuration

+

Save, and you're done! If the app will not boot, refer to Troubleshooting or head on over to the Discord.

+

Troubleshooting

+

General

+
    +
  • Ensure you have replaced empty values (UIDs, GIDs, passwords, API keys) with your own
  • +
  • Ensure proper permissions are applied within TrueNAS
  • +
  • Monitor logs via the app bash terminal during for any errors if the app is encountering issues
  • +
+

Specific Issues

+

Permissions issues inside the docker image

+

If you are encountering permissions issues with folders internal to the docker image (not your TrueNAS dataset), consider temporarily setting the user to root (user: 0). If you do this, it is recommended you fix local file permissions via shell and return access back to a non-root user.

+

In my particular setup, I had to create a user/group in TrueNAS with uid:gid of 1000:1000 and auxiliary group apps due to hard-coded values in the RomM docker image. This resolved outstanding issues I had with my instance of RomM talking to its Redis instance.

+

Contributing

+

If you have any suggestions or improvements, please submit a pull request to the RomM docs.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/System-Setup/Unraid-Compose-Setup/index.html b/4.4.0/System-Setup/Unraid-Compose-Setup/index.html new file mode 100644 index 0000000..8c579c5 --- /dev/null +++ b/4.4.0/System-Setup/Unraid-Compose-Setup/index.html @@ -0,0 +1,2304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + With Docker Compose - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

With Docker Compose

+ +

Prerequisites

+

Before getting started, install the Community Apps plugin for Unraid.

+

Install Docker Compose Addon from the CA (Community Apps Store)

+

Docker Compose Addon

+

Docker Tab

+

You will now see Compose option under the Docker Containers in your Docker Tab in Unraid

+

Docker Compose Section

+

Installation

+

Click Add New Stack

+
    +
  • +

    Name it RomM and click OK

    +
  • +
  • +

    Click the Gear Icon, edit stack, then edit the compose file

    +
  • +
+

Edit Stack

+
    +
  • +

    Add in the example Docker Compose from our example docker-compose.yml

    +
  • +
  • +

    Add your environment variables, for example: API keys, MariaDB info, and metadata providers

    +
      +
    • You can use secrets and fill in information in a separate .env file
    • +
    +
  • +
+

Edit Compose

+
    +
  • +

    Click the Gear Icon, edit stack, then edit the environment file

    +
      +
    • Fill in any environment variables if you used secrets, etc.
    • +
    +
  • +
  • +

    Make sure to click Save Changes after each edit to the compose and environment files

    +
  • +
+

Edit Env

+ +
+

Warning

+

You need to make sure you are following either Folder Structure A or B (Recommend A Type) RomM Folder Structure

+
+ +
+

Warning

+

It's strongly recommended to backup the appdata folder (or mount it in a safe location) before updating, since tearing down the container will wipe the resources (covers, screenshots, etc.)

+
+

Finish

+
    +
  • Click Compose Up
  • +
+

Compose Up Working

+

Grab IP:Port from romm, and open tab in browser in http://IP:Port and it should take you to setup screen

+

Compose Up

+

Romm Setup

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/System-Setup/Unraid-Setup-Guide/index.html b/4.4.0/System-Setup/Unraid-Setup-Guide/index.html new file mode 100644 index 0000000..ced9eb8 --- /dev/null +++ b/4.4.0/System-Setup/Unraid-Setup-Guide/index.html @@ -0,0 +1,2358 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Unraid Setup Guide - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Unraid Setup Guide

+ +

Prerequisites

+

Before getting started, install the Community Apps plugin for Unraid.

+

Docker network

+

You'll want to create a custom bridge-type network for both containers to communicate with each other. This will prevent a number of common issues Unraid users tend to come across during setup. This can be done with the following command: docker network create romm, and you can verify it worked with docker network ls.

+

console output

+

MariaDB

+

MariaDB is required to run RomM, so install it from the plugin registry. Only the official and linuxserver versions are supported, but the official version is preferred.

+

community apps search results for MariaDB

+

Now fill in all the environment variables; descriptions of the options and sensible defaults are listed in the example docker-compose.yml file.

+ +
+

Warning

+

The network type must be set to Custom: romm

+
+

MariaDB environment variables

+

Installation

+

From the Unraid dashboard, click APPS in the navigation bar. In the search bar, search for romm, and install the app listed as "OFFICIAL". This one is maintained by our team and is the most up-to-date.

+

RomM official app

+

Configuration

+

Configure the required environment variables, ports and paths as per the example docker-compose.yml file.

+ +
+

Warning

+

The network type must also be set to Custom: romm

+
+

RomM docker tab

+

Apply the changes, then head to the DOCKER tab. You should see both containers in a running state, and can access RomM using the IP:PORT of the container (highlighted below).

+

RomM and MariaDB running

+ +
+

Warning

+

It's strongly recommended to backup the appdata folder (or mount it in a safe location) before updating, since tearing down the container will wipe the resources (covers, screenshots, etc.)

+
+

Video tutorial

+

DemonWarriorTech has published How to Install RomM on Unraid (Beginner Friendly) on installing and running RomM on Unraid for Beginners with an in depth instructions and explanation of the software install and how to use it.

+

IMAGE ALT TEXT HERE

+

AlienTech42 has published a great video on installing and running RomM on Unraid. While a bit out of date vis-a-vis install instructions, it's still very useful for general setup and debugging. Check it out!

+

IMAGE ALT TEXT HERE

+

Unraid community support

+

You can find a support thread in the unraid forums.

+

Shout-outs

+

We want to give a special shout-out to @Smurre95 and @sfumat0 for their help documenting this process, and working towards getting RomM listed in CA. 🤝

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Tools/Igir-Collection-Manager/index.html b/4.4.0/Tools/Igir-Collection-Manager/index.html new file mode 100644 index 0000000..218c2c7 --- /dev/null +++ b/4.4.0/Tools/Igir-Collection-Manager/index.html @@ -0,0 +1,2418 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Igir Collection Manager - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Igir Collection Manager

+ +

Igir is a zero-setup ROM collection manager that sorts, filters, extracts or archives, patches, and reports on collections of any size on any OS. It can be used to rename your ROMs to match the RomM database, and to move them into a new directory structure.

+

Setup

+

Directory structure

+

The directory structure is important for running the bulk ROM renaming script. Before running the bulk ROM renaming script, set up your directories as follows:

+
.
+├── dats/ # DAT files from no-intro.org
+├── roms/ # Original ROM collection
+├── roms-unverified/ # Working copy of ROMs
+└── igir-romm-cleanup.sh
+
+

Initial Setup Steps

+
    +
  1. +

    Create a working copy of your ROMs:

    +
    cp -r roms/ roms-unverified/
    +
    +

    This provides a safe working environment and allows for easy script adjustment if needed.

    +
  2. +
  3. +

    Download DAT Files:

    +
      +
    • For cartridge-based systems: +
    • +
    • For optical media (e.g., PlayStation):
        +
      • Visit redump.org
      • +
      • Download platform-specific DAT files
      • +
      +
    • +
    +

    Extract the DAT files to your dats directory. You can optionally extract a subset of the .dat files into the directory instead.

    +
  4. +
+

Configuration

+

Create the cleanup script igir-romm-cleanup.sh with the contents below:

+
#!/usr/bin/env bash
+set -ou pipefail
+cd "$(dirname "${0}")"
+
+INPUT_DIR=roms-unverified
+OUTPUT_DIR=roms-verified
+
+# Documentation: https://igir.io/
+# Uses dat files: https://datomatic.no-intro.org/index.php?page=download&op=daily
+time npx -y igir@latest \
+  move \
+  extract \
+  report \
+  test \
+  -d dats/ \
+  -i "${INPUT_DIR}/" \
+  -o "${OUTPUT_DIR}/{romm}/" \
+  --input-checksum-quick false \
+  --input-checksum-min CRC32 \
+  --input-checksum-max SHA256 \
+  --only-retail
+
+

Make the script executable:

+
chmod a+x igir-romm-cleanup.sh
+
+

Usage

+

Run the script

+

Run the script. It will generate a new output directory named roms-verified, moving the files from roms-unverified if its checksum matches any of the known checksums in the DAT files provided. Any ROMs not identified will remain in the roms-unverified directory.

+

Manually move over remaining files

+

The script may not identify all of the ROMs in your input directory. You can choose to migrate them over manually:

+
npx -y igir@latest \
+  move \
+  -i roms-unverified/ \
+  -o roms-verified/ \
+  --dir-mirror
+
+

This will move your ROMs from the input to the output directory, preserving the subdirectory structure. It also cleans up file extensions in the process.

+

Reorganize multi-disc games

+

The Igir script will move games that have multiple discs to separate folders. This can confuse RomM's game detection, and those games need to be reorganized into single folders with many discs.

+

To do this enter your platform directory, such as ps or psx and run the following:

+
ls -d *Disc* | while read dir; do
+  game=$(echo "${dir}" | sed -r 's/ \(Disc [0-9]+\)//')
+  mkdir -p "${game}"
+  mv "${dir}"/* "${game}/"
+  rm -rf "${dir}"
+done
+
+

This will find any directory with (Disc in the name and move the files into a new directory without the (Disc #) string. For example:

+

Before:

+
Final Fantasy VII (Disc 1) (USA)
+Final Fantasy VII (Disc 2) (USA)
+
+

Gets combined to:

+
Final Fantasy VII (USA)
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Troubleshooting/Authentication-Issues/index.html b/4.4.0/Troubleshooting/Authentication-Issues/index.html new file mode 100644 index 0000000..0fc9083 --- /dev/null +++ b/4.4.0/Troubleshooting/Authentication-Issues/index.html @@ -0,0 +1,2219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Troubleshooting Authentication - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Authentication Issues

+ +

Error: 403 Forbidden

+

When authentication is enabled, most endpoints will return a 403 Forbidden response if you're not authenticated, or if your sessions is in a broken state. The session key can be reset by clearing your cookies.

+

CSRF protection is also enabled, which helps to mitigates CSRF attacks (useful if your instance is public). If you encounter a Forbidden (403) CSRF verification failed error, simply reloading your browser should force it to fetch a fresh CSRF cookie.

+

Error: Unable to login: CSRF token verification failed

+

This error is known to happen on Chrome, but could happen in other browsers; manually clear your cookies (specifically one called csrftoken) and hard reload your browser window (CMD+SHIFT+R on macOS, CTRL+F5 on Windows).

+

Error: 400 Bad Request on the Websocket endpoint

+

If you're running RomM behind a reverse-proxy (Caddy, Nginx, etc.), ensure that Websockets are supported and enabled. This may vary depending on the reverse proxy solution being used. In the case of Nginx Proxy Manager, enable the "Websockets Support" toggle when editing the proxy host.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Troubleshooting/Kubernetes-Issues/index.html b/4.4.0/Troubleshooting/Kubernetes-Issues/index.html new file mode 100644 index 0000000..d8d9391 --- /dev/null +++ b/4.4.0/Troubleshooting/Kubernetes-Issues/index.html @@ -0,0 +1,2193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Kubernetes Troubleshooting - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Kubernetes Issues

+ +

Error: invalid host in "tcp://<internal ip>:8080" of the "listen" directive in /etc/nginx/conf.d/default.conf:7

+

By default, Kubernetes will grab information about the service object linked to a pod and inject it as an environment variable into the pod. In RomM, this leads to the pod attempting to bind to the service IP address, leading to the above fatal error.

+

To resolve thes error, this default Kubernetes behaviour needs to be disabled by setting the enableServiceLinks value in the pod spec to false.

+
apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: romm
+  namespace: romm
+  ...
+spec:
+  ...
+  template:
+    ...
+    spec:
+      enableServiceLinks: false
+      ...
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Troubleshooting/Miscellaneous-Troubleshooting/index.html b/4.4.0/Troubleshooting/Miscellaneous-Troubleshooting/index.html new file mode 100644 index 0000000..ad47135 --- /dev/null +++ b/4.4.0/Troubleshooting/Miscellaneous-Troubleshooting/index.html @@ -0,0 +1,2220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Miscellaneous Troubleshooting - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Miscellaneous Issues

+ +

Restarting the container when using SQLite drops all the data/requires a full re-scan

+

Verify that the database is mapped to a persistent storage volume in your docker compose or Unraid template.

+
"/path/to/database:/romm/database" # [Optional] Only needed if ROMM_DB_DRIVER=sqlite or not set
+
+

Error: Could not get twitch auth token: check client_id and client_secret

+

This is likely due to mis-configured environment variables; verify that CLIENT_ID and CLIENT_SECRET are set correctly, and that both match the values in IGDB.

+

How to view RomM logs to assist with troubleshooting

+

Each platform may have different logs, but check the stdout logs for docker. For example when using the recommended docker compose setup docker logs -f romm will display a continuous stream of the log. Most lines start with INFO, WARNING, or ERROR

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Troubleshooting/Scanning-Issues/index.html b/4.4.0/Troubleshooting/Scanning-Issues/index.html new file mode 100644 index 0000000..75b7484 --- /dev/null +++ b/4.4.0/Troubleshooting/Scanning-Issues/index.html @@ -0,0 +1,2293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Troubleshooting Scanning - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Scanning Issues

+ +

Scan is skipping all platforms/ends instantly

+

There are a few common reasons why a scan may end instantly/without scanning platforms

+
    +
  • Badly mounted library: verify that you mounted your ROMs folder at /romm/library
  • +
  • Incorrect permissions: the app needs to read the files and folders in your library, check their permissions with ls -lh
  • +
  • Invalid folder structure: verify that your folder structure matches the one in the README
  • +
+

ROMs not found for platform X, check romm folder structure

+

This is the same issue as the one above, and can be quickly solved by verifying your folder structure. RomM expects a library with a folder named roms in it, for example:

+
    +
  • /server/media/library:/romm/library
  • +
  • /server/media/games/roms:/romm/library/roms
  • +
+

Scan does not recognize a platform

+

When scanning the folders mounted in /library/roms, the scanner tries to match the folder name with the platform's slug in IGDB. If you notice that the scanner isn't detecting a platform, verify that the folder name matches the slug in the URL of the platform in IGDB. For example, the Nintendo 64DD has the URL https://www.igdb.com/platforms/64dd, so the folder should be named 64dd.

+

Scan times out after ~4 hours

+

The background scan task times out after 4 hours, which can happen if you have a very large library. The easiest work around is to keep running scans every 4 hours, without checking the "Complete re-scan" option. You can also change the timeout via environment variable SCAN_TIMEOUT.

+

Scan stops before finishing a platform

+

Check the logs for RomM, you should find a line that looks like ERROR: [RomM][scan_handler][2025-04-12 11:48:55] that explains why the scanner stopped. This can often happen due to a corrupted file or a file the python zipfile library cannot handle, such as old DOS zip files with backslashes instead of forward slashes.

+

When scanning a very large library with many platforms it is difficult to keep track of which systems have scanned in

+

The easiest method is to check the logs via this command, which will list all the scanned platforms since the RomM container was started docker logs romm 2>/dev/null|egrep 'scan_handler.*Identified as.*🎮'

+

Here is an example output:

+
$ docker logs romm 2>/dev/null|egrep 'scan_handler.*Identified as.*🎮'
+INFO:     [RomM][scan_handler][2025-04-12 11:37:40]   Identified as PlayStation 🎮
+INFO:     [RomM][scan_handler][2025-04-12 14:39:32]   Identified as DOS 🎮
+INFO:     [RomM][scan_handler][2025-04-13 12:50:42]   Identified as WonderSwan 🎮
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Troubleshooting/Synology-Issues/index.html b/4.4.0/Troubleshooting/Synology-Issues/index.html new file mode 100644 index 0000000..b596c7e --- /dev/null +++ b/4.4.0/Troubleshooting/Synology-Issues/index.html @@ -0,0 +1,2193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Troubleshooting Synology - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Synology Issues

+ +

ErrNo 13: Access Denied

+

We have noticed recently a spate of access denied on Synology systems via Portainer or even docker manager. The ErrNo13 is directly related to Synology and it is a simple permission issue. To fix it please do the following:

+
    +
  1. Make sure SSH is enabled on your Synology product. Refer to here if it is not Enable SSH
  2. +
  3. Connect to SSH and login as your admin username and password (Same login used to login to DSM web page)
  4. +
  5. Take a note on your user:group you can find this by typing ID when logged into SSH.
  6. +
  7. Type the following commands in the SSH window.
  8. +
+

sudo chown -R user:group /path/to/library

+

sudo chmod -R a=,a+rX,u+w,g+w /path/to/library

+

sudo chown -R user:group /path/to/assets

+

sudo chmod -R a=,a+rX,u+w,g+w /path/to/assets

+

sudo chown -R user:group /path/to/config

+

sudo chmod -R a=,a+rX,u+w,g+w /path/to/config

+

You will find the relevant directories in your compose, this is basically the folders where you store your RomM information and we are just resetting permissions. Restart the containers and you should now have no issues scanning information in!

+

Any issues please ask in the Discord.

+

Thanks to Docker IDs - DrFrankenstein for the guidance from his blog.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Usage/Administration/index.html b/4.4.0/Usage/Administration/index.html new file mode 100644 index 0000000..49f483d --- /dev/null +++ b/4.4.0/Usage/Administration/index.html @@ -0,0 +1,2292 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Administration - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Administration

+ +

Clicking on your Profile icon from any screen opens the Administration Drawer. This drawer will look slightly different based on your role. The options available can include Profile, User Interface, Library Management, Administration, Server Stats, and About.

+

Profile

+

All users can access this screen. From here you can change your username, password, and email.

+

Additionally, you can submit your RetroAchievements username and synchronize your achievements here.

+

User Interface

+

From here you can:

+
    +
  • Change your language
  • +
  • Adjust the color scheme of RomM
  • +
  • Control which ribbons are visible on the home screen
  • +
  • Configure how to group your platforms
  • +
  • Set what information to display on game cards
  • +
  • Configure autogenerated collections and their sources
  • +
+

Library Management

+

From this screen you can:

+
    +
  • Edit platform bindings and versions on the Config tab
  • +
  • Manage missing games on the Missing Games tab
  • +
+

Administration

+

From this page you can manage users and check scheduled and manual tasks.

+

Server Stats

+

This area displays the number of platforms, games, saves, states, and screenshots on the server, as well as how much disk space this consumes. Platforms can be filtered here to show how much each platform contributes to total size and game count.

+

About

+

This pop-up displays your RomM version with links to the community Discord, GitHub, and documentation.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Usage/LibraryManagement/index.html b/4.4.0/Usage/LibraryManagement/index.html new file mode 100644 index 0000000..0f562e7 --- /dev/null +++ b/4.4.0/Usage/LibraryManagement/index.html @@ -0,0 +1,2658 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Library Management - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Library Management

+ +

The Library View

+

The default library view shows a few ribbons: Recently Added, Continue Playing, Platforms, and Collections. Games and Platforms are presented as cards that can be interacted with.

+

The menu bar is constantly visible and includes tools to Search, List Platforms, Manage Collections, Scan, go into Console Mode, and Upload content as well as access your administration panel.

+

In all views, a grid icon is present in the upper right of the screen or container. This can be toggled to collapse or expand the container.

+

Game and Platform cards

+

Platform cards can be clicked to go directly to the platform. A game card can be interacted with in several ways: putting your mouse over it allows you to download the game, play the game if the system is supported, or open a context menu giving you the option to manually match game information using a metadata agent, edit existing data, refresh the metadata, and add or remove the game from favourites or collections.

+

game card +context menu

+

Clicking a platform will take you to the platform view, while clicking a game will take you to the game view.

+

Filters

+

Filters are present on nearly every screen in RomM, allowing you to quickly drill down to see only the information relevant to your search.

+

If you perform a search first, the listed filters will be taken from the metadata of all present games. For example, if you searched for the word "Fox", and then clicked filters and selected a Language, the list of languages available will be taken from the displayed games.

+

Toggles

+

The filters that can be toggled include:

+
    +
  • Show Unmatched - Show only games that have not yet been matched at all.
  • +
  • Show Matched - Show only games that have been matched.
  • +
  • Show Favourites - Show only games that are Favourites.
  • +
  • Show Duplicates - Show only games that have multiple copies.
  • +
  • Show Playables - Show only games that can be played in a browser.
  • +
  • Show Missing - Shows only games that exist in the database but are otherwise not present.
  • +
  • Show Verified - Shows only games that have matched to Hasheous.
  • +
  • Show RetroAchievements - Shows only games that have support on the RetroAchievements website.
  • +
+

Next there is a platform dropdown. This allows you to select a platform you want to restrict your view to showing.

+

After this you can filter by metadata: Game Genre, Franchise, Collections, Company, Age Rating, Region, and Language. +One additional filter exists after these: Status. This allows you to use the "personal" data tab on any game to track your progress of the game (never played, backlogged, complete, etc).

+

The Menu bar

+

The menu bar is designed for quick access to the tools of your RomM server.

+ +

Simply typing text into the search bar will bring up game cards for each game that matches the query. To the left of the search bar are two icons: the view filters and the new collection button.

+

search bar

+

List Platforms

+

The Platforms button opens a drawer listing all of the known platforms in RomM. Clicking on any of these will create a search that is limited to that platform.

+

Collections

+

The Collections button opens a drawer listing all of your manually created collections which is specific to each user, as well as your autogenerated collections created by RomM. Clicking a collection will take you to the collection view.

+

Scan

+

This is where you'll scan for new platforms and games. Metadata sources can be selected to determine which providers will be used to fetch metadata. Use the Platforms dropdown to restrict your scan to specific platforms.

+

The Scan options dropdown allows you to choose a specific type of scan to perform:

+
    +
  • New Platforms: This will only look for platforms that are not already in RomM.
  • +
  • Quick Scan: Scans for games that are not in the library yet (fastest).
  • +
  • Unmatched Games: Attempts to match games that are not matched with the selected metadata sources.
      +
    • For example, selecting IGDB and ScreenScraper will scan games that are not matched with IGDB or ScreenScraper.
    • +
    +
  • +
  • Update Metadata: Updates the metadata for games that have been matched with selected metadata sources using the external ID (e.g. IGDB ID).
      +
    • For example, selecting IGDB and ScreenScraper will update the metadata for games that are matched with IGDB or ScreenScraper, and will use igdb_id and/or ssfr_id to refetch the metadata from the respective providers.
    • +
    +
  • +
  • Recalculate Hashes: Recalculates hashes for all files in the selected platforms.
  • +
  • Total Rescan: Rescans and rematches all games in the selected platforms (slowest).
      +
    • This will wipe all existing metadata matches, including the external IDs, and attempt to match them again, like on a fresh scan. Saves, states and notes will be preserved.
    • +
    +
  • +
+

Console

+

This button will take you to a new full screen UI that's especially good for navigation with a controller.

+

Upload

+

This button allows you to upload games directly to RomM from the user interface. Simply select a platform and then either click to browse to a ROM or drag and drop it onto the interface.

+

Recently Added

+

After a scan is performed, the most recently added games can be found on this ribbon.

+

Continue Playing

+

After a game has been played for any amount of time, it's added to this row. The context menu gains a new option for games located on this ribbon: remove from playing. This helps prevent the ribbon from being cluttered by games you wanted to test but do not plan on playing long term.

+

Platforms

+

The list of platforms presented here will be matched by RomM by directory structure. See Supported Platforms page for more information about naming your directories.

+

Favourites and Collections

+

This ribbon contains all the games you've marked as favourites, and any collections you've created or that have been generated by RomM.

+

Platform View

+

The platform view shows all of the games for a specific platform. There is a button to show the platform drawer, the platform firmware, as well as the usual filter button.

+

Platform Drawer

+

The platform view drawer has a large icon representing the platform, along with its title.

+

Below that is a button to upload ROMs and a button to initiate scans.

+

Below that is a line showing how many metadata providers are being used for that platform.

+

Next comes all the metadata for the platform itself: the name, folder name, category, generation, and so on.

+

Following that are the platform settings, which consists of the style of cover used in the game cards for that platform.

+

Finally, in the Danger Zone, is a Delete Platform button. This removes the platform from the database, it does NOT touch the files on your system. If you delete a platform in this way, scanning for new platforms will restore it, and all games will have to have their metadata re-matched.

+

platform drawer

+

Firmware

+

Next to the platform drawer button is the firmware button. This allows you to review all of the firmware for that system, and upload firmware using the user interface.

+

Uploading firmware will open a file browse dialog box. Simply select the BIOS file or files you wish to upload and click "Upload".

+

Collection View

+

The Collection View is similar to the Platform View. It has a Platform drawer next to the filters, much like how the Platform View has the Platform Drawer, but lacks the Firmware button.

+

The Collection metadata will be shown, which includes a game count, who owns the collection, the privacy of the collection, and the ability to delete the collection.

+

Game View

+

The game view is broken down into two distinct parts.

+

The first container is the poster, with the usual download, play (if the system allows for playing in a web browser) and context menu as well as a new button: copy download link.

+

The second container is the game tabs: Details, Game Data, and Personal tabs. These are containers for information about the game, how RomM sees the save data, and your personal notes. Additional tabs may be visible here depending on your configuration, including the Game Manual, data from How Long to Beat, screenshots, as well as related games. +The Details tab includes metadata for the game. This will show where the data comes from, the platform and year of release, as well as the file information like regions and collections. This is the information that is used for filtering.

+

The Game Data tab includes save files and save states, the ability to upload and download them, as well as delete them. This data is personal to the logged in user.

+

Finally the Personal tab has check boxes denoting if a game is backlogged, being played, or if it should be hidden. User data such as rating, difficulty, % completed, and the status of the game can also be set here. RetroAchievements can be viewed from this tab for supported matched games.

+

If How Long To Beat is enabled as a metadata provider, there will also be a tab here for viewing that data as well.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/Usage/UserManagement/index.html b/4.4.0/Usage/UserManagement/index.html new file mode 100644 index 0000000..e4db86e --- /dev/null +++ b/4.4.0/Usage/UserManagement/index.html @@ -0,0 +1,2234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + User Management - RomM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

User Management

+ +

The Administration panel contains the tools used to manage users on the server. The first user created will always be given the admin role.

+

Users can manage their own game saves, save states, their profile icon, and the "Personal" data tab on games.

+

Adding Users

+

Adding a user is as simple as providing a username, password, email address, and selecting a role. Users can be added manually using the "Add" button or by inviting a user via link. When inviting a user, you only need to set a role; they will be prompted to complete the rest of the information.

+

Roles Explained

+

Admin - The highest authority role. This grants full permission to do everything on the server.

+

Editor - Can edit platforms and games (adding, removing, or scanning for new content) and make collections public.

+

Viewer - The most restricted role. Can play games and interact with the Personal data tab.

+

Removing Users

+

Removing a user is as simple as clicking the red delete icon and confirming the operation.

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/4.4.0/assets/images/favicon.png b/4.4.0/assets/images/favicon.png new file mode 100644 index 0000000..1cf13b9 Binary files /dev/null and b/4.4.0/assets/images/favicon.png differ diff --git a/4.4.0/assets/images/social/API-and-Development/API-Reference.png b/4.4.0/assets/images/social/API-and-Development/API-Reference.png new file mode 100644 index 0000000..04689ea Binary files /dev/null and b/4.4.0/assets/images/social/API-and-Development/API-Reference.png differ diff --git a/4.4.0/assets/images/social/API-and-Development/Contributing.png b/4.4.0/assets/images/social/API-and-Development/Contributing.png new file mode 100644 index 0000000..b717e8e Binary files /dev/null and b/4.4.0/assets/images/social/API-and-Development/Contributing.png differ diff --git a/4.4.0/assets/images/social/API-and-Development/Development-Setup.png b/4.4.0/assets/images/social/API-and-Development/Development-Setup.png new file mode 100644 index 0000000..41d9b6f Binary files /dev/null and b/4.4.0/assets/images/social/API-and-Development/Development-Setup.png differ diff --git a/4.4.0/assets/images/social/API-and-Development/index.png b/4.4.0/assets/images/social/API-and-Development/index.png new file mode 100644 index 0000000..1e18c8b Binary files /dev/null and b/4.4.0/assets/images/social/API-and-Development/index.png differ diff --git a/4.4.0/assets/images/social/Getting-Started/Authentication.png b/4.4.0/assets/images/social/Getting-Started/Authentication.png new file mode 100644 index 0000000..4fce0b9 Binary files /dev/null and b/4.4.0/assets/images/social/Getting-Started/Authentication.png differ diff --git a/4.4.0/assets/images/social/Getting-Started/Configuration-File.png b/4.4.0/assets/images/social/Getting-Started/Configuration-File.png new file mode 100644 index 0000000..fd00472 Binary files /dev/null and b/4.4.0/assets/images/social/Getting-Started/Configuration-File.png differ diff --git a/4.4.0/assets/images/social/Getting-Started/Environment-Variables.png b/4.4.0/assets/images/social/Getting-Started/Environment-Variables.png new file mode 100644 index 0000000..da843bf Binary files /dev/null and b/4.4.0/assets/images/social/Getting-Started/Environment-Variables.png differ diff --git a/4.4.0/assets/images/social/Getting-Started/Folder-Structure.png b/4.4.0/assets/images/social/Getting-Started/Folder-Structure.png new file mode 100644 index 0000000..8191d3e Binary files /dev/null and b/4.4.0/assets/images/social/Getting-Started/Folder-Structure.png differ diff --git a/4.4.0/assets/images/social/Getting-Started/Metadata-Providers.png b/4.4.0/assets/images/social/Getting-Started/Metadata-Providers.png new file mode 100644 index 0000000..148d127 Binary files /dev/null and b/4.4.0/assets/images/social/Getting-Started/Metadata-Providers.png differ diff --git a/4.4.0/assets/images/social/Getting-Started/OIDC-Setup.png b/4.4.0/assets/images/social/Getting-Started/OIDC-Setup.png new file mode 100644 index 0000000..1fae881 Binary files /dev/null and b/4.4.0/assets/images/social/Getting-Started/OIDC-Setup.png differ diff --git a/4.4.0/assets/images/social/Getting-Started/Quick-Start-Guide.png b/4.4.0/assets/images/social/Getting-Started/Quick-Start-Guide.png new file mode 100644 index 0000000..02b7460 Binary files /dev/null and b/4.4.0/assets/images/social/Getting-Started/Quick-Start-Guide.png differ diff --git a/4.4.0/assets/images/social/Getting-Started/Reverse-Proxy.png b/4.4.0/assets/images/social/Getting-Started/Reverse-Proxy.png new file mode 100644 index 0000000..d87a1f0 Binary files /dev/null and b/4.4.0/assets/images/social/Getting-Started/Reverse-Proxy.png differ diff --git a/4.4.0/assets/images/social/Integrations/Playnite-plugin.png b/4.4.0/assets/images/social/Integrations/Playnite-plugin.png new file mode 100644 index 0000000..807b996 Binary files /dev/null and b/4.4.0/assets/images/social/Integrations/Playnite-plugin.png differ diff --git a/4.4.0/assets/images/social/Integrations/Tinfoil-integration.png b/4.4.0/assets/images/social/Integrations/Tinfoil-integration.png new file mode 100644 index 0000000..b500213 Binary files /dev/null and b/4.4.0/assets/images/social/Integrations/Tinfoil-integration.png differ diff --git a/4.4.0/assets/images/social/Integrations/muOS-app.png b/4.4.0/assets/images/social/Integrations/muOS-app.png new file mode 100644 index 0000000..4b5db64 Binary files /dev/null and b/4.4.0/assets/images/social/Integrations/muOS-app.png differ diff --git a/4.4.0/assets/images/social/Maintenance/Migrating-to-new-machine.png b/4.4.0/assets/images/social/Maintenance/Migrating-to-new-machine.png new file mode 100644 index 0000000..500d017 Binary files /dev/null and b/4.4.0/assets/images/social/Maintenance/Migrating-to-new-machine.png differ diff --git a/4.4.0/assets/images/social/Maintenance/Scheduled-Tasks.png b/4.4.0/assets/images/social/Maintenance/Scheduled-Tasks.png new file mode 100644 index 0000000..e5ee75c Binary files /dev/null and b/4.4.0/assets/images/social/Maintenance/Scheduled-Tasks.png differ diff --git a/4.4.0/assets/images/social/Maintenance/Upgrading-to-3.0.png b/4.4.0/assets/images/social/Maintenance/Upgrading-to-3.0.png new file mode 100644 index 0000000..5dcd9bf Binary files /dev/null and b/4.4.0/assets/images/social/Maintenance/Upgrading-to-3.0.png differ diff --git a/4.4.0/assets/images/social/Miscellaneous/Brand-Guidelines.png b/4.4.0/assets/images/social/Miscellaneous/Brand-Guidelines.png new file mode 100644 index 0000000..e0058ed Binary files /dev/null and b/4.4.0/assets/images/social/Miscellaneous/Brand-Guidelines.png differ diff --git a/4.4.0/assets/images/social/Miscellaneous/FAQs.png b/4.4.0/assets/images/social/Miscellaneous/FAQs.png new file mode 100644 index 0000000..6b95bdb Binary files /dev/null and b/4.4.0/assets/images/social/Miscellaneous/FAQs.png differ diff --git a/4.4.0/assets/images/social/Navigation.png b/4.4.0/assets/images/social/Navigation.png new file mode 100644 index 0000000..2ece802 Binary files /dev/null and b/4.4.0/assets/images/social/Navigation.png differ diff --git a/4.4.0/assets/images/social/OIDC-Guides/OIDC-Setup-With-Authelia.png b/4.4.0/assets/images/social/OIDC-Guides/OIDC-Setup-With-Authelia.png new file mode 100644 index 0000000..cab9add Binary files /dev/null and b/4.4.0/assets/images/social/OIDC-Guides/OIDC-Setup-With-Authelia.png differ diff --git a/4.4.0/assets/images/social/OIDC-Guides/OIDC-Setup-With-Authentik.png b/4.4.0/assets/images/social/OIDC-Guides/OIDC-Setup-With-Authentik.png new file mode 100644 index 0000000..b841d2b Binary files /dev/null and b/4.4.0/assets/images/social/OIDC-Guides/OIDC-Setup-With-Authentik.png differ diff --git a/4.4.0/assets/images/social/OIDC-Guides/OIDC-Setup-With-PocketID.png b/4.4.0/assets/images/social/OIDC-Guides/OIDC-Setup-With-PocketID.png new file mode 100644 index 0000000..09e5e0b Binary files /dev/null and b/4.4.0/assets/images/social/OIDC-Guides/OIDC-Setup-With-PocketID.png differ diff --git a/4.4.0/assets/images/social/OIDC-Guides/OIDC-Setup-With-Zitadel.png b/4.4.0/assets/images/social/OIDC-Guides/OIDC-Setup-With-Zitadel.png new file mode 100644 index 0000000..91df79b Binary files /dev/null and b/4.4.0/assets/images/social/OIDC-Guides/OIDC-Setup-With-Zitadel.png differ diff --git a/4.4.0/assets/images/social/Platforms-and-Players/Custom-Platforms.png b/4.4.0/assets/images/social/Platforms-and-Players/Custom-Platforms.png new file mode 100644 index 0000000..a2dc750 Binary files /dev/null and b/4.4.0/assets/images/social/Platforms-and-Players/Custom-Platforms.png differ diff --git a/4.4.0/assets/images/social/Platforms-and-Players/EmulatorJS-Player.png b/4.4.0/assets/images/social/Platforms-and-Players/EmulatorJS-Player.png new file mode 100644 index 0000000..156298c Binary files /dev/null and b/4.4.0/assets/images/social/Platforms-and-Players/EmulatorJS-Player.png differ diff --git a/4.4.0/assets/images/social/Platforms-and-Players/MS-DOS.png b/4.4.0/assets/images/social/Platforms-and-Players/MS-DOS.png new file mode 100644 index 0000000..37c69b5 Binary files /dev/null and b/4.4.0/assets/images/social/Platforms-and-Players/MS-DOS.png differ diff --git a/4.4.0/assets/images/social/Platforms-and-Players/RuffleRS-Player.png b/4.4.0/assets/images/social/Platforms-and-Players/RuffleRS-Player.png new file mode 100644 index 0000000..4e853a8 Binary files /dev/null and b/4.4.0/assets/images/social/Platforms-and-Players/RuffleRS-Player.png differ diff --git a/4.4.0/assets/images/social/Platforms-and-Players/Supported-Platforms.png b/4.4.0/assets/images/social/Platforms-and-Players/Supported-Platforms.png new file mode 100644 index 0000000..7714ef8 Binary files /dev/null and b/4.4.0/assets/images/social/Platforms-and-Players/Supported-Platforms.png differ diff --git a/4.4.0/assets/images/social/System-Setup/Synology-Setup-Guide.png b/4.4.0/assets/images/social/System-Setup/Synology-Setup-Guide.png new file mode 100644 index 0000000..41b67e7 Binary files /dev/null and b/4.4.0/assets/images/social/System-Setup/Synology-Setup-Guide.png differ diff --git a/4.4.0/assets/images/social/System-Setup/TrueNAS-Setup-Guide.png b/4.4.0/assets/images/social/System-Setup/TrueNAS-Setup-Guide.png new file mode 100644 index 0000000..849d788 Binary files /dev/null and b/4.4.0/assets/images/social/System-Setup/TrueNAS-Setup-Guide.png differ diff --git a/4.4.0/assets/images/social/System-Setup/Unraid-Compose-Setup.png b/4.4.0/assets/images/social/System-Setup/Unraid-Compose-Setup.png new file mode 100644 index 0000000..a413bc2 Binary files /dev/null and b/4.4.0/assets/images/social/System-Setup/Unraid-Compose-Setup.png differ diff --git a/4.4.0/assets/images/social/System-Setup/Unraid-Setup-Guide.png b/4.4.0/assets/images/social/System-Setup/Unraid-Setup-Guide.png new file mode 100644 index 0000000..6b1d60b Binary files /dev/null and b/4.4.0/assets/images/social/System-Setup/Unraid-Setup-Guide.png differ diff --git a/4.4.0/assets/images/social/Tools/Igir-Collection-Manager.png b/4.4.0/assets/images/social/Tools/Igir-Collection-Manager.png new file mode 100644 index 0000000..05fb346 Binary files /dev/null and b/4.4.0/assets/images/social/Tools/Igir-Collection-Manager.png differ diff --git a/4.4.0/assets/images/social/Troubleshooting/Authentication-Issues.png b/4.4.0/assets/images/social/Troubleshooting/Authentication-Issues.png new file mode 100644 index 0000000..64ceec6 Binary files /dev/null and b/4.4.0/assets/images/social/Troubleshooting/Authentication-Issues.png differ diff --git a/4.4.0/assets/images/social/Troubleshooting/Kubernetes-Issues.png b/4.4.0/assets/images/social/Troubleshooting/Kubernetes-Issues.png new file mode 100644 index 0000000..5b0264c Binary files /dev/null and b/4.4.0/assets/images/social/Troubleshooting/Kubernetes-Issues.png differ diff --git a/4.4.0/assets/images/social/Troubleshooting/Miscellaneous-Troubleshooting.png b/4.4.0/assets/images/social/Troubleshooting/Miscellaneous-Troubleshooting.png new file mode 100644 index 0000000..c3a80cf Binary files /dev/null and b/4.4.0/assets/images/social/Troubleshooting/Miscellaneous-Troubleshooting.png differ diff --git a/4.4.0/assets/images/social/Troubleshooting/Scanning-Issues.png b/4.4.0/assets/images/social/Troubleshooting/Scanning-Issues.png new file mode 100644 index 0000000..63d8b85 Binary files /dev/null and b/4.4.0/assets/images/social/Troubleshooting/Scanning-Issues.png differ diff --git a/4.4.0/assets/images/social/Troubleshooting/Synology-Issues.png b/4.4.0/assets/images/social/Troubleshooting/Synology-Issues.png new file mode 100644 index 0000000..f87d5b0 Binary files /dev/null and b/4.4.0/assets/images/social/Troubleshooting/Synology-Issues.png differ diff --git a/4.4.0/assets/images/social/Usage/Administration.png b/4.4.0/assets/images/social/Usage/Administration.png new file mode 100644 index 0000000..f2fb79d Binary files /dev/null and b/4.4.0/assets/images/social/Usage/Administration.png differ diff --git a/4.4.0/assets/images/social/Usage/LibraryManagement.png b/4.4.0/assets/images/social/Usage/LibraryManagement.png new file mode 100644 index 0000000..ab7e1e8 Binary files /dev/null and b/4.4.0/assets/images/social/Usage/LibraryManagement.png differ diff --git a/4.4.0/assets/images/social/Usage/UserManagement.png b/4.4.0/assets/images/social/Usage/UserManagement.png new file mode 100644 index 0000000..c42492a Binary files /dev/null and b/4.4.0/assets/images/social/Usage/UserManagement.png differ diff --git a/4.4.0/assets/images/social/index.png b/4.4.0/assets/images/social/index.png new file mode 100644 index 0000000..160e62e Binary files /dev/null and b/4.4.0/assets/images/social/index.png differ diff --git a/4.4.0/assets/javascripts/bundle.f1b6f286.min.js b/4.4.0/assets/javascripts/bundle.f1b6f286.min.js new file mode 100644 index 0000000..5082675 --- /dev/null +++ b/4.4.0/assets/javascripts/bundle.f1b6f286.min.js @@ -0,0 +1,16 @@ +"use strict";(()=>{var Wi=Object.create;var gr=Object.defineProperty;var Vi=Object.getOwnPropertyDescriptor;var Di=Object.getOwnPropertyNames,Dt=Object.getOwnPropertySymbols,Ni=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,ao=Object.prototype.propertyIsEnumerable;var io=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,$=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&io(e,r,t[r]);if(Dt)for(var r of Dt(t))ao.call(t,r)&&io(e,r,t[r]);return e};var so=(e,t)=>{var r={};for(var o in e)yr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Dt)for(var o of Dt(e))t.indexOf(o)<0&&ao.call(e,o)&&(r[o]=e[o]);return r};var xr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var zi=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Di(t))!yr.call(e,n)&&n!==r&&gr(e,n,{get:()=>t[n],enumerable:!(o=Vi(t,n))||o.enumerable});return e};var Mt=(e,t,r)=>(r=e!=null?Wi(Ni(e)):{},zi(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var co=(e,t,r)=>new Promise((o,n)=>{var i=p=>{try{s(r.next(p))}catch(c){n(c)}},a=p=>{try{s(r.throw(p))}catch(c){n(c)}},s=p=>p.done?o(p.value):Promise.resolve(p.value).then(i,a);s((r=r.apply(e,t)).next())});var lo=xr((Er,po)=>{(function(e,t){typeof Er=="object"&&typeof po!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Er,function(){"use strict";function e(r){var o=!0,n=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(k){return!!(k&&k!==document&&k.nodeName!=="HTML"&&k.nodeName!=="BODY"&&"classList"in k&&"contains"in k.classList)}function p(k){var ft=k.type,qe=k.tagName;return!!(qe==="INPUT"&&a[ft]&&!k.readOnly||qe==="TEXTAREA"&&!k.readOnly||k.isContentEditable)}function c(k){k.classList.contains("focus-visible")||(k.classList.add("focus-visible"),k.setAttribute("data-focus-visible-added",""))}function l(k){k.hasAttribute("data-focus-visible-added")&&(k.classList.remove("focus-visible"),k.removeAttribute("data-focus-visible-added"))}function f(k){k.metaKey||k.altKey||k.ctrlKey||(s(r.activeElement)&&c(r.activeElement),o=!0)}function u(k){o=!1}function d(k){s(k.target)&&(o||p(k.target))&&c(k.target)}function y(k){s(k.target)&&(k.target.classList.contains("focus-visible")||k.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(k.target))}function L(k){document.visibilityState==="hidden"&&(n&&(o=!0),X())}function X(){document.addEventListener("mousemove",J),document.addEventListener("mousedown",J),document.addEventListener("mouseup",J),document.addEventListener("pointermove",J),document.addEventListener("pointerdown",J),document.addEventListener("pointerup",J),document.addEventListener("touchmove",J),document.addEventListener("touchstart",J),document.addEventListener("touchend",J)}function ee(){document.removeEventListener("mousemove",J),document.removeEventListener("mousedown",J),document.removeEventListener("mouseup",J),document.removeEventListener("pointermove",J),document.removeEventListener("pointerdown",J),document.removeEventListener("pointerup",J),document.removeEventListener("touchmove",J),document.removeEventListener("touchstart",J),document.removeEventListener("touchend",J)}function J(k){k.target.nodeName&&k.target.nodeName.toLowerCase()==="html"||(o=!1,ee())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",L,!0),X(),r.addEventListener("focus",d,!0),r.addEventListener("blur",y,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var qr=xr((hy,On)=>{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var $a=/["'&<>]/;On.exports=Pa;function Pa(e){var t=""+e,r=$a.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof It=="object"&&typeof Yr=="object"?Yr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof It=="object"?It.ClipboardJS=r():t.ClipboardJS=r()})(It,function(){return function(){var e={686:function(o,n,i){"use strict";i.d(n,{default:function(){return Ui}});var a=i(279),s=i.n(a),p=i(370),c=i.n(p),l=i(817),f=i.n(l);function u(D){try{return document.execCommand(D)}catch(A){return!1}}var d=function(A){var M=f()(A);return u("cut"),M},y=d;function L(D){var A=document.documentElement.getAttribute("dir")==="rtl",M=document.createElement("textarea");M.style.fontSize="12pt",M.style.border="0",M.style.padding="0",M.style.margin="0",M.style.position="absolute",M.style[A?"right":"left"]="-9999px";var F=window.pageYOffset||document.documentElement.scrollTop;return M.style.top="".concat(F,"px"),M.setAttribute("readonly",""),M.value=D,M}var X=function(A,M){var F=L(A);M.container.appendChild(F);var V=f()(F);return u("copy"),F.remove(),V},ee=function(A){var M=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},F="";return typeof A=="string"?F=X(A,M):A instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(A==null?void 0:A.type)?F=X(A.value,M):(F=f()(A),u("copy")),F},J=ee;function k(D){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?k=function(M){return typeof M}:k=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},k(D)}var ft=function(){var A=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},M=A.action,F=M===void 0?"copy":M,V=A.container,Y=A.target,$e=A.text;if(F!=="copy"&&F!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Y!==void 0)if(Y&&k(Y)==="object"&&Y.nodeType===1){if(F==="copy"&&Y.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(F==="cut"&&(Y.hasAttribute("readonly")||Y.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if($e)return J($e,{container:V});if(Y)return F==="cut"?y(Y):J(Y,{container:V})},qe=ft;function Fe(D){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Fe=function(M){return typeof M}:Fe=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},Fe(D)}function ki(D,A){if(!(D instanceof A))throw new TypeError("Cannot call a class as a function")}function no(D,A){for(var M=0;M0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof V.action=="function"?V.action:this.defaultAction,this.target=typeof V.target=="function"?V.target:this.defaultTarget,this.text=typeof V.text=="function"?V.text:this.defaultText,this.container=Fe(V.container)==="object"?V.container:document.body}},{key:"listenClick",value:function(V){var Y=this;this.listener=c()(V,"click",function($e){return Y.onClick($e)})}},{key:"onClick",value:function(V){var Y=V.delegateTarget||V.currentTarget,$e=this.action(Y)||"copy",Vt=qe({action:$e,container:this.container,target:this.target(Y),text:this.text(Y)});this.emit(Vt?"success":"error",{action:$e,text:Vt,trigger:Y,clearSelection:function(){Y&&Y.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(V){return vr("action",V)}},{key:"defaultTarget",value:function(V){var Y=vr("target",V);if(Y)return document.querySelector(Y)}},{key:"defaultText",value:function(V){return vr("text",V)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(V){var Y=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return J(V,Y)}},{key:"cut",value:function(V){return y(V)}},{key:"isSupported",value:function(){var V=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Y=typeof V=="string"?[V]:V,$e=!!document.queryCommandSupported;return Y.forEach(function(Vt){$e=$e&&!!document.queryCommandSupported(Vt)}),$e}}]),M}(s()),Ui=Fi},828:function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,p){for(;s&&s.nodeType!==n;){if(typeof s.matches=="function"&&s.matches(p))return s;s=s.parentNode}}o.exports=a},438:function(o,n,i){var a=i(828);function s(l,f,u,d,y){var L=c.apply(this,arguments);return l.addEventListener(u,L,y),{destroy:function(){l.removeEventListener(u,L,y)}}}function p(l,f,u,d,y){return typeof l.addEventListener=="function"?s.apply(null,arguments):typeof u=="function"?s.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(L){return s(L,f,u,d,y)}))}function c(l,f,u,d){return function(y){y.delegateTarget=a(y.target,f),y.delegateTarget&&d.call(l,y)}}o.exports=p},879:function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(o,n,i){var a=i(879),s=i(438);function p(u,d,y){if(!u&&!d&&!y)throw new Error("Missing required arguments");if(!a.string(d))throw new TypeError("Second argument must be a String");if(!a.fn(y))throw new TypeError("Third argument must be a Function");if(a.node(u))return c(u,d,y);if(a.nodeList(u))return l(u,d,y);if(a.string(u))return f(u,d,y);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(u,d,y){return u.addEventListener(d,y),{destroy:function(){u.removeEventListener(d,y)}}}function l(u,d,y){return Array.prototype.forEach.call(u,function(L){L.addEventListener(d,y)}),{destroy:function(){Array.prototype.forEach.call(u,function(L){L.removeEventListener(d,y)})}}}function f(u,d,y){return s(document.body,u,d,y)}o.exports=p},817:function(o){function n(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var p=window.getSelection(),c=document.createRange();c.selectNodeContents(i),p.removeAllRanges(),p.addRange(c),a=p.toString()}return a}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,a,s){var p=this.e||(this.e={});return(p[i]||(p[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var p=this;function c(){p.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),p=0,c=s.length;for(p;p0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function N(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],a;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(s){a={error:s}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(a)throw a.error}}return i}function q(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||p(d,L)})},y&&(n[d]=y(n[d])))}function p(d,y){try{c(o[d](y))}catch(L){u(i[0][3],L)}}function c(d){d.value instanceof nt?Promise.resolve(d.value.v).then(l,f):u(i[0][2],d)}function l(d){p("next",d)}function f(d){p("throw",d)}function u(d,y){d(y),i.shift(),i.length&&p(i[0][0],i[0][1])}}function uo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof he=="function"?he(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(a){return new Promise(function(s,p){a=e[i](a),n(s,p,a.done,a.value)})}}function n(i,a,s,p){Promise.resolve(p).then(function(c){i({value:c,done:s})},a)}}function H(e){return typeof e=="function"}function ut(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var zt=ut(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Qe(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ue=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=he(a),p=s.next();!p.done;p=s.next()){var c=p.value;c.remove(this)}}catch(L){t={error:L}}finally{try{p&&!p.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var l=this.initialTeardown;if(H(l))try{l()}catch(L){i=L instanceof zt?L.errors:[L]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=he(f),d=u.next();!d.done;d=u.next()){var y=d.value;try{ho(y)}catch(L){i=i!=null?i:[],L instanceof zt?i=q(q([],N(i)),N(L.errors)):i.push(L)}}}catch(L){o={error:L}}finally{try{d&&!d.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new zt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)ho(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Qe(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Qe(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Tr=Ue.EMPTY;function qt(e){return e instanceof Ue||e&&"closed"in e&&H(e.remove)&&H(e.add)&&H(e.unsubscribe)}function ho(e){H(e)?e():e.unsubscribe()}var Pe={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var dt={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,a=n.isStopped,s=n.observers;return i||a?Tr:(this.currentObservers=null,s.push(r),new Ue(function(){o.currentObservers=null,Qe(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,a=o.isStopped;n?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new j;return r.source=this,r},t.create=function(r,o){return new To(r,o)},t}(j);var To=function(e){oe(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:Tr},t}(g);var _r=function(e){oe(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t}(g);var At={now:function(){return(At.delegate||Date).now()},delegate:void 0};var Ct=function(e){oe(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=At);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,a=o._infiniteTimeWindow,s=o._timestampProvider,p=o._windowTime;n||(i.push(r),!a&&i.push(s.now()+p)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,a=n._buffer,s=a.slice(),p=0;p0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t}(gt);var Lo=function(e){oe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(yt);var kr=new Lo(Oo);var Mo=function(e){oe(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=vt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var a=r.actions;o!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==o&&(vt.cancelAnimationFrame(o),r._scheduled=void 0)},t}(gt);var _o=function(e){oe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o=this._scheduled;this._scheduled=void 0;var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t}(yt);var me=new _o(Mo);var S=new j(function(e){return e.complete()});function Yt(e){return e&&H(e.schedule)}function Hr(e){return e[e.length-1]}function Xe(e){return H(Hr(e))?e.pop():void 0}function ke(e){return Yt(Hr(e))?e.pop():void 0}function Bt(e,t){return typeof Hr(e)=="number"?e.pop():t}var xt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Gt(e){return H(e==null?void 0:e.then)}function Jt(e){return H(e[bt])}function Xt(e){return Symbol.asyncIterator&&H(e==null?void 0:e[Symbol.asyncIterator])}function Zt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var er=Zi();function tr(e){return H(e==null?void 0:e[er])}function rr(e){return fo(this,arguments,function(){var r,o,n,i;return Nt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,nt(r.read())];case 3:return o=a.sent(),n=o.value,i=o.done,i?[4,nt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,nt(n)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function or(e){return H(e==null?void 0:e.getReader)}function U(e){if(e instanceof j)return e;if(e!=null){if(Jt(e))return ea(e);if(xt(e))return ta(e);if(Gt(e))return ra(e);if(Xt(e))return Ao(e);if(tr(e))return oa(e);if(or(e))return na(e)}throw Zt(e)}function ea(e){return new j(function(t){var r=e[bt]();if(H(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function ta(e){return new j(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?b(function(n,i){return e(n,i,o)}):le,Te(1),r?Ve(t):Qo(function(){return new ir}))}}function jr(e){return e<=0?function(){return S}:E(function(t,r){var o=[];t.subscribe(T(r,function(n){o.push(n),e=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new g}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,p=s===void 0?!0:s;return function(c){var l,f,u,d=0,y=!1,L=!1,X=function(){f==null||f.unsubscribe(),f=void 0},ee=function(){X(),l=u=void 0,y=L=!1},J=function(){var k=l;ee(),k==null||k.unsubscribe()};return E(function(k,ft){d++,!L&&!y&&X();var qe=u=u!=null?u:r();ft.add(function(){d--,d===0&&!L&&!y&&(f=Ur(J,p))}),qe.subscribe(ft),!l&&d>0&&(l=new at({next:function(Fe){return qe.next(Fe)},error:function(Fe){L=!0,X(),f=Ur(ee,n,Fe),qe.error(Fe)},complete:function(){y=!0,X(),f=Ur(ee,a),qe.complete()}}),U(k).subscribe(l))})(c)}}function Ur(e,t){for(var r=[],o=2;oe.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function R(e,t=document){let r=fe(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function fe(e,t=document){return t.querySelector(e)||void 0}function Ie(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var wa=O(h(document.body,"focusin"),h(document.body,"focusout")).pipe(_e(1),Q(void 0),m(()=>Ie()||document.body),G(1));function et(e){return wa.pipe(m(t=>e.contains(t)),K())}function $t(e,t){return C(()=>O(h(e,"mouseenter").pipe(m(()=>!0)),h(e,"mouseleave").pipe(m(()=>!1))).pipe(t?Ht(r=>Le(+!r*t)):le,Q(e.matches(":hover"))))}function Jo(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Jo(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)Jo(o,n);return o}function sr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function Tt(e){let t=x("script",{src:e});return C(()=>(document.head.appendChild(t),O(h(t,"load"),h(t,"error").pipe(v(()=>$r(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),_(()=>document.head.removeChild(t)),Te(1))))}var Xo=new g,Ta=C(()=>typeof ResizeObserver=="undefined"?Tt("https://unpkg.com/resize-observer-polyfill"):I(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>Xo.next(t)))),v(e=>O(Ye,I(e)).pipe(_(()=>e.disconnect()))),G(1));function ce(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return Ta.pipe(w(r=>r.observe(t)),v(r=>Xo.pipe(b(o=>o.target===t),_(()=>r.unobserve(t)))),m(()=>ce(e)),Q(ce(e)))}function St(e){return{width:e.scrollWidth,height:e.scrollHeight}}function cr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Zo(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function De(e){return{x:e.offsetLeft,y:e.offsetTop}}function en(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function tn(e){return O(h(window,"load"),h(window,"resize")).pipe(Me(0,me),m(()=>De(e)),Q(De(e)))}function pr(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ne(e){return O(h(e,"scroll"),h(window,"scroll"),h(window,"resize")).pipe(Me(0,me),m(()=>pr(e)),Q(pr(e)))}var rn=new g,Sa=C(()=>I(new IntersectionObserver(e=>{for(let t of e)rn.next(t)},{threshold:0}))).pipe(v(e=>O(Ye,I(e)).pipe(_(()=>e.disconnect()))),G(1));function tt(e){return Sa.pipe(w(t=>t.observe(e)),v(t=>rn.pipe(b(({target:r})=>r===e),_(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function on(e,t=16){return Ne(e).pipe(m(({y:r})=>{let o=ce(e),n=St(e);return r>=n.height-o.height-t}),K())}var lr={drawer:R("[data-md-toggle=drawer]"),search:R("[data-md-toggle=search]")};function nn(e){return lr[e].checked}function Je(e,t){lr[e].checked!==t&&lr[e].click()}function ze(e){let t=lr[e];return h(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function Oa(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function La(){return O(h(window,"compositionstart").pipe(m(()=>!0)),h(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function an(){let e=h(window,"keydown").pipe(b(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:nn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),b(({mode:t,type:r})=>{if(t==="global"){let o=Ie();if(typeof o!="undefined")return!Oa(o,r)}return!0}),pe());return La().pipe(v(t=>t?S:e))}function ye(){return new URL(location.href)}function lt(e,t=!1){if(B("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function sn(){return new g}function cn(){return location.hash.slice(1)}function pn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Ma(e){return O(h(window,"hashchange"),e).pipe(m(cn),Q(cn()),b(t=>t.length>0),G(1))}function ln(e){return Ma(e).pipe(m(t=>fe(`[id="${t}"]`)),b(t=>typeof t!="undefined"))}function Pt(e){let t=matchMedia(e);return ar(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function mn(){let e=matchMedia("print");return O(h(window,"beforeprint").pipe(m(()=>!0)),h(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function Nr(e,t){return e.pipe(v(r=>r?t():S))}function zr(e,t){return new j(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let a=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+a*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function je(e,t){return zr(e,t).pipe(v(r=>r.text()),m(r=>JSON.parse(r)),G(1))}function fn(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),G(1))}function un(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),G(1))}function dn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function hn(){return O(h(window,"scroll",{passive:!0}),h(window,"resize",{passive:!0})).pipe(m(dn),Q(dn()))}function bn(){return{width:innerWidth,height:innerHeight}}function vn(){return h(window,"resize",{passive:!0}).pipe(m(bn),Q(bn()))}function gn(){return z([hn(),vn()]).pipe(m(([e,t])=>({offset:e,size:t})),G(1))}function mr(e,{viewport$:t,header$:r}){let o=t.pipe(te("size")),n=z([o,r]).pipe(m(()=>De(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:a,size:s},{x:p,y:c}])=>({offset:{x:a.x-p,y:a.y-c+i},size:s})))}function _a(e){return h(e,"message",t=>t.data)}function Aa(e){let t=new g;return t.subscribe(r=>e.postMessage(r)),t}function yn(e,t=new Worker(e)){let r=_a(t),o=Aa(t),n=new g;n.subscribe(o);let i=o.pipe(Z(),ie(!0));return n.pipe(Z(),Re(r.pipe(W(i))),pe())}var Ca=R("#__config"),Ot=JSON.parse(Ca.textContent);Ot.base=`${new URL(Ot.base,ye())}`;function xe(){return Ot}function B(e){return Ot.features.includes(e)}function Ee(e,t){return typeof t!="undefined"?Ot.translations[e].replace("#",t.toString()):Ot.translations[e]}function Se(e,t=document){return R(`[data-md-component=${e}]`,t)}function ae(e,t=document){return P(`[data-md-component=${e}]`,t)}function ka(e){let t=R(".md-typeset > :first-child",e);return h(t,"click",{once:!0}).pipe(m(()=>R(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function xn(e){if(!B("announce.dismiss")||!e.childElementCount)return S;if(!e.hidden){let t=R(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return C(()=>{let t=new g;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),ka(e).pipe(w(r=>t.next(r)),_(()=>t.complete()),m(r=>$({ref:e},r)))})}function Ha(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function En(e,t){let r=new g;return r.subscribe(({hidden:o})=>{e.hidden=o}),Ha(e,t).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))}function Rt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function wn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function Tn(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Rt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Rt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function Sn(e){return x("button",{class:"md-clipboard md-icon",title:Ee("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}var Ln=Mt(qr());function Qr(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(p=>!e.terms[p]).reduce((p,c)=>[...p,x("del",null,(0,Ln.default)(c))," "],[]).slice(0,-1),i=xe(),a=new URL(e.location,i.base);B("search.highlight")&&a.searchParams.set("h",Object.entries(e.terms).filter(([,p])=>p).reduce((p,[c])=>`${p} ${c}`.trim(),""));let{tags:s}=xe();return x("a",{href:`${a}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&x("nav",{class:"md-tags"},e.tags.map(p=>{let c=s?p in s?`md-tag-icon md-tag--${s[p]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${c}`},p)})),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Ee("search.result.term.missing"),": ",...n)))}function Mn(e){let t=e[0].score,r=[...e],o=xe(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),a=r.findIndex(l=>l.scoreQr(l,1)),...p.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,p.length>0&&p.length===1?Ee("search.result.more.one"):Ee("search.result.more.other",p.length))),...p.map(l=>Qr(l,1)))]:[]];return x("li",{class:"md-search-result__item"},c)}function _n(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?sr(r):r)))}function Kr(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function An(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Ra(e){var o;let t=xe(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Cn(e,t){var o;let r=xe();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Ee("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Ra)))}var Ia=0;function ja(e){let t=z([et(e),$t(e)]).pipe(m(([o,n])=>o||n),K()),r=C(()=>Zo(e)).pipe(ne(Ne),pt(1),He(t),m(()=>en(e)));return t.pipe(Ae(o=>o),v(()=>z([t,r])),m(([o,n])=>({active:o,offset:n})),pe())}function Fa(e,t){let{content$:r,viewport$:o}=t,n=`__tooltip2_${Ia++}`;return C(()=>{let i=new g,a=new _r(!1);i.pipe(Z(),ie(!1)).subscribe(a);let s=a.pipe(Ht(c=>Le(+!c*250,kr)),K(),v(c=>c?r:S),w(c=>c.id=n),pe());z([i.pipe(m(({active:c})=>c)),s.pipe(v(c=>$t(c,250)),Q(!1))]).pipe(m(c=>c.some(l=>l))).subscribe(a);let p=a.pipe(b(c=>c),re(s,o),m(([c,l,{size:f}])=>{let u=e.getBoundingClientRect(),d=u.width/2;if(l.role==="tooltip")return{x:d,y:8+u.height};if(u.y>=f.height/2){let{height:y}=ce(l);return{x:d,y:-16-y}}else return{x:d,y:16+u.height}}));return z([s,i,p]).subscribe(([c,{offset:l},f])=>{c.style.setProperty("--md-tooltip-host-x",`${l.x}px`),c.style.setProperty("--md-tooltip-host-y",`${l.y}px`),c.style.setProperty("--md-tooltip-x",`${f.x}px`),c.style.setProperty("--md-tooltip-y",`${f.y}px`),c.classList.toggle("md-tooltip2--top",f.y<0),c.classList.toggle("md-tooltip2--bottom",f.y>=0)}),a.pipe(b(c=>c),re(s,(c,l)=>l),b(c=>c.role==="tooltip")).subscribe(c=>{let l=ce(R(":scope > *",c));c.style.setProperty("--md-tooltip-width",`${l.width}px`),c.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(K(),ve(me),re(s)).subscribe(([c,l])=>{l.classList.toggle("md-tooltip2--active",c)}),z([a.pipe(b(c=>c)),s]).subscribe(([c,l])=>{l.role==="dialog"?(e.setAttribute("aria-controls",n),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",n)}),a.pipe(b(c=>!c)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ja(e).pipe(w(c=>i.next(c)),_(()=>i.complete()),m(c=>$({ref:e},c)))})}function mt(e,{viewport$:t},r=document.body){return Fa(e,{content$:new j(o=>{let n=e.title,i=wn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t})}function Ua(e,t){let r=C(()=>z([tn(e),Ne(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:a,height:s}=ce(e);return{x:o-i.x+a/2,y:n-i.y+s/2}}));return et(e).pipe(v(o=>r.pipe(m(n=>({active:o,offset:n})),Te(+!o||1/0))))}function kn(e,t,{target$:r}){let[o,n]=Array.from(e.children);return C(()=>{let i=new g,a=i.pipe(Z(),ie(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),tt(e).pipe(W(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),O(i.pipe(b(({active:s})=>s)),i.pipe(_e(250),b(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe(Me(16,me)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(pt(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),h(n,"click").pipe(W(a),b(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),h(n,"mousedown").pipe(W(a),re(i)).subscribe(([s,{active:p}])=>{var c;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(p){s.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(c=Ie())==null||c.blur()}}),r.pipe(W(a),b(s=>s===o),Ge(125)).subscribe(()=>e.focus()),Ua(e,t).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))})}function Wa(e){return e.tagName==="CODE"?P(".c, .c1, .cm",e):[e]}function Va(e){let t=[];for(let r of Wa(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,p]=a;if(typeof p=="undefined"){let c=i.splitText(a.index);i=c.splitText(s.length),t.push(c)}else{i.textContent=s,t.push(i);break}}}}return t}function Hn(e,t){t.append(...Array.from(e.childNodes))}function fr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,a=new Map;for(let s of Va(t)){let[,p]=s.textContent.match(/\((\d+)\)/);fe(`:scope > li:nth-child(${p})`,e)&&(a.set(p,Tn(p,i)),s.replaceWith(a.get(p)))}return a.size===0?S:C(()=>{let s=new g,p=s.pipe(Z(),ie(!0)),c=[];for(let[l,f]of a)c.push([R(".md-typeset",f),R(`:scope > li:nth-child(${l})`,e)]);return o.pipe(W(p)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of c)l?Hn(f,u):Hn(u,f)}),O(...[...a].map(([,l])=>kn(l,t,{target$:r}))).pipe(_(()=>s.complete()),pe())})}function $n(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return $n(t)}}function Pn(e,t){return C(()=>{let r=$n(e);return typeof r!="undefined"?fr(r,e,t):S})}var Rn=Mt(Br());var Da=0;function In(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return In(t)}}function Na(e){return ge(e).pipe(m(({width:t})=>({scrollable:St(e).width>t})),te("scrollable"))}function jn(e,t){let{matches:r}=matchMedia("(hover)"),o=C(()=>{let n=new g,i=n.pipe(jr(1));n.subscribe(({scrollable:c})=>{c&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[];if(Rn.default.isSupported()&&(e.closest(".copy")||B("content.code.copy")&&!e.closest(".no-copy"))){let c=e.closest("pre");c.id=`__code_${Da++}`;let l=Sn(c.id);c.insertBefore(l,e),B("content.tooltips")&&a.push(mt(l,{viewport$}))}let s=e.closest(".highlight");if(s instanceof HTMLElement){let c=In(s);if(typeof c!="undefined"&&(s.classList.contains("annotate")||B("content.code.annotate"))){let l=fr(c,e,t);a.push(ge(s).pipe(W(i),m(({width:f,height:u})=>f&&u),K(),v(f=>f?l:S)))}}return P(":scope > span[id]",e).length&&e.classList.add("md-code__content"),Na(e).pipe(w(c=>n.next(c)),_(()=>n.complete()),m(c=>$({ref:e},c)),Re(...a))});return B("content.lazy")?tt(e).pipe(b(n=>n),Te(1),v(()=>o)):o}function za(e,{target$:t,print$:r}){let o=!0;return O(t.pipe(m(n=>n.closest("details:not([open])")),b(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(b(n=>n||!o),w(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Fn(e,t){return C(()=>{let r=new g;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),za(e,t).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}var Un=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color);stroke-width:.05rem}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.entityTitleText{fill:var(--md-mermaid-label-fg-color)}.attributeBoxEven,.attributeBoxOdd{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var Gr,Qa=0;function Ka(){return typeof mermaid=="undefined"||mermaid instanceof Element?Tt("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):I(void 0)}function Wn(e){return e.classList.remove("mermaid"),Gr||(Gr=Ka().pipe(w(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Un,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),G(1))),Gr.subscribe(()=>co(this,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${Qa++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),a=r.attachShadow({mode:"closed"});a.innerHTML=n,e.replaceWith(r),i==null||i(a)})),Gr.pipe(m(()=>({ref:e})))}var Vn=x("table");function Dn(e){return e.replaceWith(Vn),Vn.replaceWith(An(e)),I({ref:e})}function Ya(e){let t=e.find(r=>r.checked)||e[0];return O(...e.map(r=>h(r,"change").pipe(m(()=>R(`label[for="${r.id}"]`))))).pipe(Q(R(`label[for="${t.id}"]`)),m(r=>({active:r})))}function Nn(e,{viewport$:t,target$:r}){let o=R(".tabbed-labels",e),n=P(":scope > input",e),i=Kr("prev");e.append(i);let a=Kr("next");return e.append(a),C(()=>{let s=new g,p=s.pipe(Z(),ie(!0));z([s,ge(e),tt(e)]).pipe(W(p),Me(1,me)).subscribe({next([{active:c},l]){let f=De(c),{width:u}=ce(c);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let d=pr(o);(f.xd.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([Ne(o),ge(o)]).pipe(W(p)).subscribe(([c,l])=>{let f=St(o);i.hidden=c.x<16,a.hidden=c.x>f.width-l.width-16}),O(h(i,"click").pipe(m(()=>-1)),h(a,"click").pipe(m(()=>1))).pipe(W(p)).subscribe(c=>{let{width:l}=ce(o);o.scrollBy({left:l*c,behavior:"smooth"})}),r.pipe(W(p),b(c=>n.includes(c))).subscribe(c=>c.click()),o.classList.add("tabbed-labels--linked");for(let c of n){let l=R(`label[for="${c.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),h(l.firstElementChild,"click").pipe(W(p),b(f=>!(f.metaKey||f.ctrlKey)),w(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return B("content.tabs.link")&&s.pipe(Ce(1),re(t)).subscribe(([{active:c},{offset:l}])=>{let f=c.innerText.trim();if(c.hasAttribute("data-md-switching"))c.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let y of P("[data-tabs]"))for(let L of P(":scope > input",y)){let X=R(`label[for="${L.id}"]`);if(X!==c&&X.innerText.trim()===f){X.setAttribute("data-md-switching",""),L.click();break}}window.scrollTo({top:e.offsetTop-u});let d=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...d])])}}),s.pipe(W(p)).subscribe(()=>{for(let c of P("audio, video",e))c.pause()}),Ya(n).pipe(w(c=>s.next(c)),_(()=>s.complete()),m(c=>$({ref:e},c)))}).pipe(Ke(se))}function zn(e,{viewport$:t,target$:r,print$:o}){return O(...P(".annotate:not(.highlight)",e).map(n=>Pn(n,{target$:r,print$:o})),...P("pre:not(.mermaid) > code",e).map(n=>jn(n,{target$:r,print$:o})),...P("pre.mermaid",e).map(n=>Wn(n)),...P("table:not([class])",e).map(n=>Dn(n)),...P("details",e).map(n=>Fn(n,{target$:r,print$:o})),...P("[data-tabs]",e).map(n=>Nn(n,{viewport$:t,target$:r})),...P("[title]",e).filter(()=>B("content.tooltips")).map(n=>mt(n,{viewport$:t})))}function Ba(e,{alert$:t}){return t.pipe(v(r=>O(I(!0),I(!1).pipe(Ge(2e3))).pipe(m(o=>({message:r,active:o})))))}function qn(e,t){let r=R(".md-typeset",e);return C(()=>{let o=new g;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),Ba(e,t).pipe(w(n=>o.next(n)),_(()=>o.complete()),m(n=>$({ref:e},n)))})}var Ga=0;function Ja(e,t){document.body.append(e);let{width:r}=ce(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=cr(t),n=typeof o!="undefined"?Ne(o):I({x:0,y:0}),i=O(et(t),$t(t)).pipe(K());return z([i,n]).pipe(m(([a,s])=>{let{x:p,y:c}=De(t),l=ce(t),f=t.closest("table");return f&&t.parentElement&&(p+=f.offsetLeft+t.parentElement.offsetLeft,c+=f.offsetTop+t.parentElement.offsetTop),{active:a,offset:{x:p-s.x+l.width/2-r/2,y:c-s.y+l.height+8}}}))}function Qn(e){let t=e.title;if(!t.length)return S;let r=`__tooltip_${Ga++}`,o=Rt(r,"inline"),n=R(".md-typeset",o);return n.innerHTML=t,C(()=>{let i=new g;return i.subscribe({next({offset:a}){o.style.setProperty("--md-tooltip-x",`${a.x}px`),o.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),O(i.pipe(b(({active:a})=>a)),i.pipe(_e(250),b(({active:a})=>!a))).subscribe({next({active:a}){a?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe(Me(16,me)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(pt(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?o.style.setProperty("--md-tooltip-0",`${-a}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),Ja(o,e).pipe(w(a=>i.next(a)),_(()=>i.complete()),m(a=>$({ref:e},a)))}).pipe(Ke(se))}function Xa({viewport$:e}){if(!B("header.autohide"))return I(!1);let t=e.pipe(m(({offset:{y:n}})=>n),Be(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),K()),o=ze("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),K(),v(n=>n?r:I(!1)),Q(!1))}function Kn(e,t){return C(()=>z([ge(e),Xa(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),K((r,o)=>r.height===o.height&&r.hidden===o.hidden),G(1))}function Yn(e,{header$:t,main$:r}){return C(()=>{let o=new g,n=o.pipe(Z(),ie(!0));o.pipe(te("active"),He(t)).subscribe(([{active:a},{hidden:s}])=>{e.classList.toggle("md-header--shadow",a&&!s),e.hidden=s});let i=ue(P("[title]",e)).pipe(b(()=>B("content.tooltips")),ne(a=>Qn(a)));return r.subscribe(o),t.pipe(W(n),m(a=>$({ref:e},a)),Re(i.pipe(W(n))))})}function Za(e,{viewport$:t,header$:r}){return mr(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=ce(e);return{active:o>=n}}),te("active"))}function Bn(e,t){return C(()=>{let r=new g;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=fe(".md-content h1");return typeof o=="undefined"?S:Za(o,t).pipe(w(n=>r.next(n)),_(()=>r.complete()),m(n=>$({ref:e},n)))})}function Gn(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),K()),n=o.pipe(v(()=>ge(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),te("bottom"))));return z([o,n,t]).pipe(m(([i,{top:a,bottom:s},{offset:{y:p},size:{height:c}}])=>(c=Math.max(0,c-Math.max(0,a-p,i)-Math.max(0,c+p-s)),{offset:a-i,height:c,active:a-i<=p})),K((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function es(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return I(...e).pipe(ne(o=>h(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),G(1))}function Jn(e){let t=P("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=Pt("(prefers-color-scheme: light)");return C(()=>{let i=new g;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),p=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=p.getAttribute("data-md-color-scheme"),a.color.primary=p.getAttribute("data-md-color-primary"),a.color.accent=p.getAttribute("data-md-color-accent")}for(let[s,p]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,p);for(let s=0;sa.key==="Enter"),re(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(m(()=>{let a=Se("header"),s=window.getComputedStyle(a);return o.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(p=>(+p).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(ve(se)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),es(t).pipe(W(n.pipe(Ce(1))),ct(),w(a=>i.next(a)),_(()=>i.complete()),m(a=>$({ref:e},a)))})}function Xn(e,{progress$:t}){return C(()=>{let r=new g;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(w(o=>r.next({value:o})),_(()=>r.complete()),m(o=>({ref:e,value:o})))})}var Jr=Mt(Br());function ts(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function Zn({alert$:e}){Jr.default.isSupported()&&new j(t=>{new Jr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||ts(R(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(w(t=>{t.trigger.focus()}),m(()=>Ee("clipboard.copied"))).subscribe(e)}function ei(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function rs(e,t){let r=new Map;for(let o of P("url",e)){let n=R("loc",o),i=[ei(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",o)){let s=a.getAttribute("href");s!=null&&i.push(ei(new URL(s),t))}}return r}function ur(e){return un(new URL("sitemap.xml",e)).pipe(m(t=>rs(t,new URL(e))),de(()=>I(new Map)))}function os(e,t){if(!(e.target instanceof Element))return S;let r=e.target.closest("a");if(r===null)return S;if(r.target||e.metaKey||e.ctrlKey)return S;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),I(new URL(r.href))):S}function ti(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function ri(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return I(e)}function ns(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...B("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=fe(o),i=fe(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=ti(document);for(let[o,n]of ti(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Se("container");return We(P("script",r)).pipe(v(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new j(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),S}),Z(),ie(document))}function oi({location$:e,viewport$:t,progress$:r}){let o=xe();if(location.protocol==="file:")return S;let n=ur(o.base);I(document).subscribe(ri);let i=h(document.body,"click").pipe(He(n),v(([p,c])=>os(p,c)),pe()),a=h(window,"popstate").pipe(m(ye),pe());i.pipe(re(t)).subscribe(([p,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",p)}),O(i,a).subscribe(e);let s=e.pipe(te("pathname"),v(p=>fn(p,{progress$:r}).pipe(de(()=>(lt(p,!0),S)))),v(ri),v(ns),pe());return O(s.pipe(re(e,(p,c)=>c)),s.pipe(v(()=>e),te("hash")),e.pipe(K((p,c)=>p.pathname===c.pathname&&p.hash===c.hash),v(()=>i),w(()=>history.back()))).subscribe(p=>{var c,l;history.state!==null||!p.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",pn(p.hash),history.scrollRestoration="manual")}),e.subscribe(()=>{history.scrollRestoration="manual"}),h(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),t.pipe(te("offset"),_e(100)).subscribe(({offset:p})=>{history.replaceState(p,"")}),s}var ni=Mt(qr());function ii(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,a)=>`${i}${a}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(0,ni.default)(a).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function jt(e){return e.type===1}function dr(e){return e.type===3}function ai(e,t){let r=yn(e);return O(I(location.protocol!=="file:"),ze("search")).pipe(Ae(o=>o),v(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:B("search.suggest")}}})),r}function si(e){var l;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:o,currentBaseURL:n}=e,i=(l=Xr(n))==null?void 0:l.pathname;if(i===void 0)return;let a=ss(o.pathname,i);if(a===void 0)return;let s=ps(t.keys());if(!t.has(s))return;let p=Xr(a,s);if(!p||!t.has(p.href))return;let c=Xr(a,r);if(c)return c.hash=o.hash,c.search=o.search,c}function Xr(e,t){try{return new URL(e,t)}catch(r){return}}function ss(e,t){if(e.startsWith(t))return e.slice(t.length)}function cs(e,t){let r=Math.min(e.length,t.length),o;for(o=0;oS)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:a,aliases:s})=>a===i||s.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),v(n=>h(document.body,"click").pipe(b(i=>!i.metaKey&&!i.ctrlKey),re(o),v(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&n.has(s.href)){let p=s.href;return!i.target.closest(".md-version")&&n.get(p)===a?S:(i.preventDefault(),I(new URL(p)))}}return S}),v(i=>ur(i).pipe(m(a=>{var s;return(s=si({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(n=>lt(n,!0)),z([r,o]).subscribe(([n,i])=>{R(".md-header__topic").appendChild(Cn(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var s;let i=new URL(t.base),a=__md_get("__outdated",sessionStorage,i);if(a===null){a=!0;let p=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(p)||(p=[p]);e:for(let c of p)for(let l of n.aliases.concat(n.version))if(new RegExp(c,"i").test(l)){a=!1;break e}__md_set("__outdated",a,sessionStorage,i)}if(a)for(let p of ae("outdated"))p.hidden=!1})}function ls(e,{worker$:t}){let{searchParams:r}=ye();r.has("q")&&(Je("search",!0),e.value=r.get("q"),e.focus(),ze("search").pipe(Ae(i=>!i)).subscribe(()=>{let i=ye();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=et(e),n=O(t.pipe(Ae(jt)),h(e,"keyup"),o).pipe(m(()=>e.value),K());return z([n,o]).pipe(m(([i,a])=>({value:i,focus:a})),G(1))}function pi(e,{worker$:t}){let r=new g,o=r.pipe(Z(),ie(!0));z([t.pipe(Ae(jt)),r],(i,a)=>a).pipe(te("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(te("focus")).subscribe(({focus:i})=>{i&&Je("search",i)}),h(e.form,"reset").pipe(W(o)).subscribe(()=>e.focus());let n=R("header [for=__search]");return h(n,"click").subscribe(()=>e.focus()),ls(e,{worker$:t}).pipe(w(i=>r.next(i)),_(()=>r.complete()),m(i=>$({ref:e},i)),G(1))}function li(e,{worker$:t,query$:r}){let o=new g,n=on(e.parentElement).pipe(b(Boolean)),i=e.parentElement,a=R(":scope > :first-child",e),s=R(":scope > :last-child",e);ze("search").subscribe(l=>s.setAttribute("role",l?"list":"presentation")),o.pipe(re(r),Wr(t.pipe(Ae(jt)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:a.textContent=f.length?Ee("search.result.none"):Ee("search.result.placeholder");break;case 1:a.textContent=Ee("search.result.one");break;default:let u=sr(l.length);a.textContent=Ee("search.result.other",u)}});let p=o.pipe(w(()=>s.innerHTML=""),v(({items:l})=>O(I(...l.slice(0,10)),I(...l.slice(10)).pipe(Be(4),Dr(n),v(([f])=>f)))),m(Mn),pe());return p.subscribe(l=>s.appendChild(l)),p.pipe(ne(l=>{let f=fe("details",l);return typeof f=="undefined"?S:h(f,"toggle").pipe(W(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(b(dr),m(({data:l})=>l)).pipe(w(l=>o.next(l)),_(()=>o.complete()),m(l=>$({ref:e},l)))}function ms(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=ye();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function mi(e,t){let r=new g,o=r.pipe(Z(),ie(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),h(e,"click").pipe(W(o)).subscribe(n=>n.preventDefault()),ms(e,t).pipe(w(n=>r.next(n)),_(()=>r.complete()),m(n=>$({ref:e},n)))}function fi(e,{worker$:t,keyboard$:r}){let o=new g,n=Se("search-query"),i=O(h(n,"keydown"),h(n,"focus")).pipe(ve(se),m(()=>n.value),K());return o.pipe(He(i),m(([{suggest:s},p])=>{let c=p.split(/([\s-]+)/);if(s!=null&&s.length&&c[c.length-1]){let l=s[s.length-1];l.startsWith(c[c.length-1])&&(c[c.length-1]=l)}else c.length=0;return c})).subscribe(s=>e.innerHTML=s.join("").replace(/\s/g," ")),r.pipe(b(({mode:s})=>s==="search")).subscribe(s=>{switch(s.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(b(dr),m(({data:s})=>s)).pipe(w(s=>o.next(s)),_(()=>o.complete()),m(()=>({ref:e})))}function ui(e,{index$:t,keyboard$:r}){let o=xe();try{let n=ai(o.search,t),i=Se("search-query",e),a=Se("search-result",e);h(e,"click").pipe(b(({target:p})=>p instanceof Element&&!!p.closest("a"))).subscribe(()=>Je("search",!1)),r.pipe(b(({mode:p})=>p==="search")).subscribe(p=>{let c=Ie();switch(p.type){case"Enter":if(c===i){let l=new Map;for(let f of P(":first-child [href]",a)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,d])=>d-u);f.click()}p.claim()}break;case"Escape":case"Tab":Je("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof c=="undefined")i.focus();else{let l=[i,...P(":not(details) > [href], summary, details[open] [href]",a)],f=Math.max(0,(Math.max(0,l.indexOf(c))+l.length+(p.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}p.claim();break;default:i!==Ie()&&i.focus()}}),r.pipe(b(({mode:p})=>p==="global")).subscribe(p=>{switch(p.type){case"f":case"s":case"/":i.focus(),i.select(),p.claim();break}});let s=pi(i,{worker$:n});return O(s,li(a,{worker$:n,query$:s})).pipe(Re(...ae("search-share",e).map(p=>mi(p,{query$:s})),...ae("search-suggest",e).map(p=>fi(p,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,Ye}}function di(e,{index$:t,location$:r}){return z([t,r.pipe(Q(ye()),b(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>ii(o.config)(n.searchParams.get("h"))),m(o=>{var a;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let p=s.textContent,c=o(p);c.length>p.length&&n.set(s,c)}for(let[s,p]of n){let{childNodes:c}=x("span",null,p);s.replaceWith(...Array.from(c))}return{ref:e,nodes:n}}))}function fs(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(n,Math.max(0,s-i))-n,{height:a,locked:s>=i+n})),K((i,a)=>i.height===a.height&&i.locked===a.locked))}function Zr(e,o){var n=o,{header$:t}=n,r=so(n,["header$"]);let i=R(".md-sidebar__scrollwrap",e),{y:a}=De(i);return C(()=>{let s=new g,p=s.pipe(Z(),ie(!0)),c=s.pipe(Me(0,me));return c.pipe(re(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),c.pipe(Ae()).subscribe(()=>{for(let l of P(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=ce(f);f.scrollTo({top:u-d/2})}}}),ue(P("label[tabindex]",e)).pipe(ne(l=>h(l,"click").pipe(ve(se),m(()=>l),W(p)))).subscribe(l=>{let f=R(`[id="${l.htmlFor}"]`);R(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),fs(e,r).pipe(w(l=>s.next(l)),_(()=>s.complete()),m(l=>$({ref:e},l)))})}function hi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return st(je(`${r}/releases/latest`).pipe(de(()=>S),m(o=>({version:o.tag_name})),Ve({})),je(r).pipe(de(()=>S),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),Ve({}))).pipe(m(([o,n])=>$($({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return je(r).pipe(m(o=>({repositories:o.public_repos})),Ve({}))}}function bi(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return st(je(`${r}/releases/permalink/latest`).pipe(de(()=>S),m(({tag_name:o})=>({version:o})),Ve({})),je(r).pipe(de(()=>S),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Ve({}))).pipe(m(([o,n])=>$($({},o),n)))}function vi(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return hi(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return bi(r,o)}return S}var us;function ds(e){return us||(us=C(()=>{let t=__md_get("__source",sessionStorage);if(t)return I(t);if(ae("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return S}return vi(e.href).pipe(w(o=>__md_set("__source",o,sessionStorage)))}).pipe(de(()=>S),b(t=>Object.keys(t).length>0),m(t=>({facts:t})),G(1)))}function gi(e){let t=R(":scope > :last-child",e);return C(()=>{let r=new g;return r.subscribe(({facts:o})=>{t.appendChild(_n(o)),t.classList.add("md-source__repository--active")}),ds(e).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}function hs(e,{viewport$:t,header$:r}){return ge(document.body).pipe(v(()=>mr(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),te("hidden"))}function yi(e,t){return C(()=>{let r=new g;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(B("navigation.tabs.sticky")?I({hidden:!1}):hs(e,t)).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}function bs(e,{viewport$:t,header$:r}){let o=new Map,n=P(".md-nav__link",e);for(let s of n){let p=decodeURIComponent(s.hash.substring(1)),c=fe(`[id="${p}"]`);typeof c!="undefined"&&o.set(s,c)}let i=r.pipe(te("height"),m(({height:s})=>{let p=Se("main"),c=R(":scope > :first-child",p);return s+.8*(c.offsetTop-p.offsetTop)}),pe());return ge(document.body).pipe(te("height"),v(s=>C(()=>{let p=[];return I([...o].reduce((c,[l,f])=>{for(;p.length&&o.get(p[p.length-1]).tagName>=f.tagName;)p.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let d=f.offsetParent;for(;d;d=d.offsetParent)u+=d.offsetTop;return c.set([...p=[...p,l]].reverse(),u)},new Map))}).pipe(m(p=>new Map([...p].sort(([,c],[,l])=>c-l))),He(i),v(([p,c])=>t.pipe(Fr(([l,f],{offset:{y:u},size:d})=>{let y=u+d.height>=Math.floor(s.height);for(;f.length;){let[,L]=f[0];if(L-c=u&&!y)f=[l.pop(),...f];else break}return[l,f]},[[],[...p]]),K((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([s,p])=>({prev:s.map(([c])=>c),next:p.map(([c])=>c)})),Q({prev:[],next:[]}),Be(2,1),m(([s,p])=>s.prev.length{let i=new g,a=i.pipe(Z(),ie(!0));if(i.subscribe(({prev:s,next:p})=>{for(let[c]of p)c.classList.remove("md-nav__link--passed"),c.classList.remove("md-nav__link--active");for(let[c,[l]]of s.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",c===s.length-1)}),B("toc.follow")){let s=O(t.pipe(_e(1),m(()=>{})),t.pipe(_e(250),m(()=>"smooth")));i.pipe(b(({prev:p})=>p.length>0),He(o.pipe(ve(se))),re(s)).subscribe(([[{prev:p}],c])=>{let[l]=p[p.length-1];if(l.offsetHeight){let f=cr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=ce(f);f.scrollTo({top:u-d/2,behavior:c})}}})}return B("navigation.tracking")&&t.pipe(W(a),te("offset"),_e(250),Ce(1),W(n.pipe(Ce(1))),ct({delay:250}),re(i)).subscribe(([,{prev:s}])=>{let p=ye(),c=s[s.length-1];if(c&&c.length){let[l]=c,{hash:f}=new URL(l.href);p.hash!==f&&(p.hash=f,history.replaceState({},"",`${p}`))}else p.hash="",history.replaceState({},"",`${p}`)}),bs(e,{viewport$:t,header$:r}).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))})}function vs(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:a}})=>a),Be(2,1),m(([a,s])=>a>s&&s>0),K()),i=r.pipe(m(({active:a})=>a));return z([i,n]).pipe(m(([a,s])=>!(a&&s)),K(),W(o.pipe(Ce(1))),ie(!0),ct({delay:250}),m(a=>({hidden:a})))}function Ei(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new g,a=i.pipe(Z(),ie(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(W(a),te("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),h(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),vs(e,{viewport$:t,main$:o,target$:n}).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))}function wi({document$:e,viewport$:t}){e.pipe(v(()=>P(".md-ellipsis")),ne(r=>tt(r).pipe(W(e.pipe(Ce(1))),b(o=>o),m(()=>r),Te(1))),b(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,B("content.tooltips")?mt(n,{viewport$:t}).pipe(W(e.pipe(Ce(1))),_(()=>n.removeAttribute("title"))):S})).subscribe(),B("content.tooltips")&&e.pipe(v(()=>P(".md-status")),ne(r=>mt(r,{viewport$:t}))).subscribe()}function Ti({document$:e,tablet$:t}){e.pipe(v(()=>P(".md-toggle--indeterminate")),w(r=>{r.indeterminate=!0,r.checked=!1}),ne(r=>h(r,"change").pipe(Vr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),re(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function gs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Si({document$:e}){e.pipe(v(()=>P("[data-md-scrollfix]")),w(t=>t.removeAttribute("data-md-scrollfix")),b(gs),ne(t=>h(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function Oi({viewport$:e,tablet$:t}){z([ze("search"),t]).pipe(m(([r,o])=>r&&!o),v(r=>I(r).pipe(Ge(r?400:100))),re(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function ys(){return location.protocol==="file:"?Tt(`${new URL("search/search_index.js",eo.base)}`).pipe(m(()=>__index),G(1)):je(new URL("search/search_index.json",eo.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ot=Go(),Ut=sn(),Lt=ln(Ut),to=an(),Oe=gn(),hr=Pt("(min-width: 960px)"),Mi=Pt("(min-width: 1220px)"),_i=mn(),eo=xe(),Ai=document.forms.namedItem("search")?ys():Ye,ro=new g;Zn({alert$:ro});var oo=new g;B("navigation.instant")&&oi({location$:Ut,viewport$:Oe,progress$:oo}).subscribe(ot);var Li;((Li=eo.version)==null?void 0:Li.provider)==="mike"&&ci({document$:ot});O(Ut,Lt).pipe(Ge(125)).subscribe(()=>{Je("drawer",!1),Je("search",!1)});to.pipe(b(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=fe("link[rel=prev]");typeof t!="undefined"&<(t);break;case"n":case".":let r=fe("link[rel=next]");typeof r!="undefined"&<(r);break;case"Enter":let o=Ie();o instanceof HTMLLabelElement&&o.click()}});wi({viewport$:Oe,document$:ot});Ti({document$:ot,tablet$:hr});Si({document$:ot});Oi({viewport$:Oe,tablet$:hr});var rt=Kn(Se("header"),{viewport$:Oe}),Ft=ot.pipe(m(()=>Se("main")),v(e=>Gn(e,{viewport$:Oe,header$:rt})),G(1)),xs=O(...ae("consent").map(e=>En(e,{target$:Lt})),...ae("dialog").map(e=>qn(e,{alert$:ro})),...ae("palette").map(e=>Jn(e)),...ae("progress").map(e=>Xn(e,{progress$:oo})),...ae("search").map(e=>ui(e,{index$:Ai,keyboard$:to})),...ae("source").map(e=>gi(e))),Es=C(()=>O(...ae("announce").map(e=>xn(e)),...ae("content").map(e=>zn(e,{viewport$:Oe,target$:Lt,print$:_i})),...ae("content").map(e=>B("search.highlight")?di(e,{index$:Ai,location$:Ut}):S),...ae("header").map(e=>Yn(e,{viewport$:Oe,header$:rt,main$:Ft})),...ae("header-title").map(e=>Bn(e,{viewport$:Oe,header$:rt})),...ae("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Nr(Mi,()=>Zr(e,{viewport$:Oe,header$:rt,main$:Ft})):Nr(hr,()=>Zr(e,{viewport$:Oe,header$:rt,main$:Ft}))),...ae("tabs").map(e=>yi(e,{viewport$:Oe,header$:rt})),...ae("toc").map(e=>xi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Lt})),...ae("top").map(e=>Ei(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Lt})))),Ci=ot.pipe(v(()=>Es),Re(xs),G(1));Ci.subscribe();window.document$=ot;window.location$=Ut;window.target$=Lt;window.keyboard$=to;window.viewport$=Oe;window.tablet$=hr;window.screen$=Mi;window.print$=_i;window.alert$=ro;window.progress$=oo;window.component$=Ci;})(); +//# sourceMappingURL=bundle.f1b6f286.min.js.map + diff --git a/4.4.0/assets/javascripts/bundle.f1b6f286.min.js.map b/4.4.0/assets/javascripts/bundle.f1b6f286.min.js.map new file mode 100644 index 0000000..2644bf1 --- /dev/null +++ b/4.4.0/assets/javascripts/bundle.f1b6f286.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/escape-html/index.js", "node_modules/clipboard/dist/clipboard.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/tslib/tslib.es6.mjs", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/findurl/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*\n * Copyright (c) 2016-2025 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 960px)\")\nconst screen$ = watchMedia(\"(min-width: 1220px)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/******************************************************************************\nCopyright (c) Microsoft Corporation.\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THIS SOFTWARE.\n***************************************************************************** */\n/* global Reflect, Promise, SuppressedError, Symbol, Iterator */\n\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nexport function __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\nexport var __assign = function() {\n __assign = Object.assign || function __assign(t) {\n for (var s, i = 1, n = arguments.length; i < n; i++) {\n s = arguments[i];\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\n }\n return t;\n }\n return __assign.apply(this, arguments);\n}\n\nexport function __rest(s, e) {\n var t = {};\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\n t[p] = s[p];\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\n t[p[i]] = s[p[i]];\n }\n return t;\n}\n\nexport function __decorate(decorators, target, key, desc) {\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\n return c > 3 && r && Object.defineProperty(target, key, r), r;\n}\n\nexport function __param(paramIndex, decorator) {\n return function (target, key) { decorator(target, key, paramIndex); }\n}\n\nexport function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {\n function accept(f) { if (f !== void 0 && typeof f !== \"function\") throw new TypeError(\"Function expected\"); return f; }\n var kind = contextIn.kind, key = kind === \"getter\" ? \"get\" : kind === \"setter\" ? \"set\" : \"value\";\n var target = !descriptorIn && ctor ? contextIn[\"static\"] ? ctor : ctor.prototype : null;\n var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});\n var _, done = false;\n for (var i = decorators.length - 1; i >= 0; i--) {\n var context = {};\n for (var p in contextIn) context[p] = p === \"access\" ? {} : contextIn[p];\n for (var p in contextIn.access) context.access[p] = contextIn.access[p];\n context.addInitializer = function (f) { if (done) throw new TypeError(\"Cannot add initializers after decoration has completed\"); extraInitializers.push(accept(f || null)); };\n var result = (0, decorators[i])(kind === \"accessor\" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);\n if (kind === \"accessor\") {\n if (result === void 0) continue;\n if (result === null || typeof result !== \"object\") throw new TypeError(\"Object expected\");\n if (_ = accept(result.get)) descriptor.get = _;\n if (_ = accept(result.set)) descriptor.set = _;\n if (_ = accept(result.init)) initializers.unshift(_);\n }\n else if (_ = accept(result)) {\n if (kind === \"field\") initializers.unshift(_);\n else descriptor[key] = _;\n }\n }\n if (target) Object.defineProperty(target, contextIn.name, descriptor);\n done = true;\n};\n\nexport function __runInitializers(thisArg, initializers, value) {\n var useValue = arguments.length > 2;\n for (var i = 0; i < initializers.length; i++) {\n value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);\n }\n return useValue ? value : void 0;\n};\n\nexport function __propKey(x) {\n return typeof x === \"symbol\" ? x : \"\".concat(x);\n};\n\nexport function __setFunctionName(f, name, prefix) {\n if (typeof name === \"symbol\") name = name.description ? \"[\".concat(name.description, \"]\") : \"\";\n return Object.defineProperty(f, \"name\", { configurable: true, value: prefix ? \"\".concat(prefix, \" \", name) : name });\n};\n\nexport function __metadata(metadataKey, metadataValue) {\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\n}\n\nexport function __awaiter(thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n}\n\nexport function __generator(thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === \"function\" ? Iterator : Object).prototype);\n return g.next = verb(0), g[\"throw\"] = verb(1), g[\"return\"] = verb(2), typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n}\n\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n var desc = Object.getOwnPropertyDescriptor(m, k);\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\n desc = { enumerable: true, get: function() { return m[k]; } };\n }\n Object.defineProperty(o, k2, desc);\n}) : (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n o[k2] = m[k];\n});\n\nexport function __exportStar(m, o) {\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\n}\n\nexport function __values(o) {\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\n if (m) return m.call(o);\n if (o && typeof o.length === \"number\") return {\n next: function () {\n if (o && i >= o.length) o = void 0;\n return { value: o && o[i++], done: !o };\n }\n };\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\n}\n\nexport function __read(o, n) {\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\n if (!m) return o;\n var i = m.call(o), r, ar = [], e;\n try {\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\n }\n catch (error) { e = { error: error }; }\n finally {\n try {\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\n }\n finally { if (e) throw e.error; }\n }\n return ar;\n}\n\n/** @deprecated */\nexport function __spread() {\n for (var ar = [], i = 0; i < arguments.length; i++)\n ar = ar.concat(__read(arguments[i]));\n return ar;\n}\n\n/** @deprecated */\nexport function __spreadArrays() {\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\n r[k] = a[j];\n return r;\n}\n\nexport function __spreadArray(to, from, pack) {\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\n if (ar || !(i in from)) {\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\n ar[i] = from[i];\n }\n }\n return to.concat(ar || Array.prototype.slice.call(from));\n}\n\nexport function __await(v) {\n return this instanceof __await ? (this.v = v, this) : new __await(v);\n}\n\nexport function __asyncGenerator(thisArg, _arguments, generator) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\n return i = Object.create((typeof AsyncIterator === \"function\" ? AsyncIterator : Object).prototype), verb(\"next\"), verb(\"throw\"), verb(\"return\", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;\n function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }\n function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\n function fulfill(value) { resume(\"next\", value); }\n function reject(value) { resume(\"throw\", value); }\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\n}\n\nexport function __asyncDelegator(o) {\n var i, p;\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }\n}\n\nexport function __asyncValues(o) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var m = o[Symbol.asyncIterator], i;\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\n}\n\nexport function __makeTemplateObject(cooked, raw) {\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\n return cooked;\n};\n\nvar __setModuleDefault = Object.create ? (function(o, v) {\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\n}) : function(o, v) {\n o[\"default\"] = v;\n};\n\nexport function __importStar(mod) {\n if (mod && mod.__esModule) return mod;\n var result = {};\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\n __setModuleDefault(result, mod);\n return result;\n}\n\nexport function __importDefault(mod) {\n return (mod && mod.__esModule) ? mod : { default: mod };\n}\n\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n}\n\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n}\n\nexport function __classPrivateFieldIn(state, receiver) {\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\n}\n\nexport function __addDisposableResource(env, value, async) {\n if (value !== null && value !== void 0) {\n if (typeof value !== \"object\" && typeof value !== \"function\") throw new TypeError(\"Object expected.\");\n var dispose, inner;\n if (async) {\n if (!Symbol.asyncDispose) throw new TypeError(\"Symbol.asyncDispose is not defined.\");\n dispose = value[Symbol.asyncDispose];\n }\n if (dispose === void 0) {\n if (!Symbol.dispose) throw new TypeError(\"Symbol.dispose is not defined.\");\n dispose = value[Symbol.dispose];\n if (async) inner = dispose;\n }\n if (typeof dispose !== \"function\") throw new TypeError(\"Object not disposable.\");\n if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };\n env.stack.push({ value: value, dispose: dispose, async: async });\n }\n else if (async) {\n env.stack.push({ async: true });\n }\n return value;\n}\n\nvar _SuppressedError = typeof SuppressedError === \"function\" ? SuppressedError : function (error, suppressed, message) {\n var e = new Error(message);\n return e.name = \"SuppressedError\", e.error = error, e.suppressed = suppressed, e;\n};\n\nexport function __disposeResources(env) {\n function fail(e) {\n env.error = env.hasError ? new _SuppressedError(e, env.error, \"An error was suppressed during disposal.\") : e;\n env.hasError = true;\n }\n var r, s = 0;\n function next() {\n while (r = env.stack.pop()) {\n try {\n if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);\n if (r.dispose) {\n var result = r.dispose.call(r.value);\n if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });\n }\n else s |= 1;\n }\n catch (e) {\n fail(e);\n }\n }\n if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();\n if (env.hasError) throw env.error;\n }\n return next();\n}\n\nexport default {\n __extends,\n __assign,\n __rest,\n __decorate,\n __param,\n __metadata,\n __awaiter,\n __generator,\n __createBinding,\n __exportStar,\n __values,\n __read,\n __spread,\n __spreadArrays,\n __spreadArray,\n __await,\n __asyncGenerator,\n __asyncDelegator,\n __asyncValues,\n __makeTemplateObject,\n __importStar,\n __importDefault,\n __classPrivateFieldGet,\n __classPrivateFieldSet,\n __classPrivateFieldIn,\n __addDisposableResource,\n __disposeResources,\n};\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n *\n * @class Subscription\n */\nexport class Subscription implements SubscriptionLike {\n /** @nocollapse */\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n * @return {void}\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n *\n * @class Subscriber\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @nocollapse\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param {T} [value] The `next` value.\n * @return {void}\n */\n next(value?: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param {any} [err] The `error` exception.\n * @return {void}\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n * @return {void}\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as (((value: T) => void) | undefined),\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent\n * @param subscriber The stopped subscriber\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n *\n * @class Observable\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @constructor\n * @param {Function} subscribe the function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @owner Observable\n * @method create\n * @param {Function} subscribe? the subscriber function to be passed to the Observable constructor\n * @return {Observable} a new observable\n * @nocollapse\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @method lift\n * @param operator the operator defining the operation to take on the observable\n * @return a new observable with the Operator applied\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param {Observer|Function} observerOrNext (optional) Either an observer with methods to be called,\n * or the first of three possible handlers, which is the handler for each value emitted from the subscribed\n * Observable.\n * @param {Function} error (optional) A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param {Function} complete (optional) A handler for a terminal event resulting from successful completion.\n * @return {Subscription} a subscription reference to the registered handlers\n * @method subscribe\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next a handler for each value emitted by the observable\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @method Symbol.observable\n * @return {Observable} this instance of the observable\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n * @method pipe\n * @return {Observable} the Observable result of all of the operators having\n * been called in the order they were passed in.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @method toPromise\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @nocollapse\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return {Observable} Observable that the Subject casts to\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\n/**\n * @class AnonymousSubject\n */\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n *\n * @class BehaviorSubject\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param bufferSize The size of the buffer to replay on subscription\n * @param windowTime The amount of time the buffered items will stay buffered\n * @param timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n *\n * @class Action\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler.\n * @return {void}\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n * @return {any}\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @class Scheduler\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return {number} A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param {function(state: ?T): ?Subscription} work A function representing a\n * task, or some unit of work to be executed by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler itself.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @return {Subscription} A subscription in order to be able to unsubscribe\n * the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @type {boolean}\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @type {any}\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n const flushId = this._scheduled;\n this._scheduled = undefined;\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an