Added nise-replay-viewer
@ -13,8 +13,7 @@ import java.util.*
|
||||
|
||||
@Service
|
||||
class RssService(
|
||||
private val dslContext: DSLContext,
|
||||
private val scoreService: ScoreService
|
||||
private val dslContext: DSLContext
|
||||
) {
|
||||
|
||||
fun generateFeed(): RssFeed {
|
||||
|
||||
@ -5,12 +5,6 @@ IMAGE_NAME="nise-frontend"
|
||||
IMAGE_REGISTRY="git.gengo.tech/nuff"
|
||||
IMAGE_VERSION="latest"
|
||||
|
||||
# Check if there are uncommitted changes
|
||||
if [[ -n $(git status --porcelain) ]]; then
|
||||
echo "Error: There are uncommitted changes. Please commit them before building."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf target/
|
||||
|
||||
# Clean and build Angular project
|
||||
|
||||
33
nise-replay-viewer/Build.sh
Executable file
@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Set variables
|
||||
IMAGE_NAME="nise-replay-viewer"
|
||||
IMAGE_REGISTRY="git.gengo.tech/nuff"
|
||||
IMAGE_VERSION="latest"
|
||||
|
||||
rm -rf dist/
|
||||
|
||||
# Clean and build Angular project
|
||||
source /usr/share/nvm/init-nvm.sh
|
||||
nvm use 18.19
|
||||
npm run build
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ng build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build and push Docker image
|
||||
docker build . -t $IMAGE_NAME:$IMAGE_VERSION
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Docker build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker tag $IMAGE_NAME:$IMAGE_VERSION $IMAGE_REGISTRY/$IMAGE_NAME:$IMAGE_VERSION
|
||||
docker push $IMAGE_REGISTRY/$IMAGE_NAME:$IMAGE_VERSION
|
||||
if [ "$?" != "0" ]; then
|
||||
echo "Error: Failed to push $IMAGE_REGISTRY/$IMAGE_NAME:$IMAGE_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Docker image pushed successfully to $IMAGE_REGISTRY/$IMAGE_NAME:$IMAGE_VERSION"
|
||||
10
nise-replay-viewer/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
||||
FROM openresty/openresty:focal
|
||||
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
|
||||
COPY dist/ /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
46
nise-replay-viewer/README.md
Normal file
@ -0,0 +1,46 @@
|
||||

|
||||
|
||||

|
||||
|
||||
Replay Inspector is a beatmap/replay analyzer and editor for [osu!](https://osu.ppy.sh/) and is currently in development phase.
|
||||
|
||||
> Inspector is NOT a fork of abstrakt8's rewind, and it has been developed from ground up using same utilites. This project was developed with simplicity in mind using beginner friendly frameworks and easy structure.
|
||||
|
||||
## Geting started
|
||||
|
||||
To use the public version of Replay Inspector, you can follow [click](https://edit.assist.games).
|
||||
|
||||
If you wanna clone and use it locally, proceed by
|
||||
|
||||
```
|
||||
git clone https://github.com/cunev/replay-inspector
|
||||
```
|
||||
|
||||
then install packages
|
||||
|
||||
```
|
||||
bun install
|
||||
```
|
||||
|
||||
and run the editor
|
||||
|
||||
|
||||
```
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Features
|
||||
- View replay path and beatmap
|
||||
- Seek through replay
|
||||
- View clicks
|
||||
- Enable/disable hardrock
|
||||
- Modify map metadata
|
||||
- Analyze clicks, frametimes and release times
|
||||
|
||||
## In development
|
||||
- Export replays
|
||||
- Modify path
|
||||
- Modify keypresses
|
||||
- Music playback
|
||||
- Shortcuts
|
||||
|
||||
16
nise-replay-viewer/components.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/interface/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
BIN
nise-replay-viewer/dp12.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
26
nise-replay-viewer/index.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Replay Inspector</title>
|
||||
<link rel="icon" type="image/x-icon" href="https://nise.moe/assets/favicon.ico">
|
||||
<!-- Embed data -->
|
||||
<meta property="og:title" content="/nise.moe/ - osu!cheaters finder">
|
||||
<meta property="og:description" content="crawls osu!std replays and tries to find naughty boys.">
|
||||
<meta property="og:url" content="https://nise.moe">
|
||||
<meta property="og:image" content="https://nise.moe/assets/banner.png">
|
||||
<meta property="og:type" content="website">
|
||||
<meta name="theme-color" content="#151515">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image:src" content="https://nise.moe/assets/banner.png">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
29
nise-replay-viewer/nginx.conf
Normal file
@ -0,0 +1,29 @@
|
||||
server {
|
||||
|
||||
gzip on;
|
||||
gzip_static on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
gzip_proxied no-cache no-store private expired auth;
|
||||
gzip_min_length 1000;
|
||||
|
||||
listen 80;
|
||||
|
||||
resolver local=on ipv6=off;
|
||||
resolver_timeout 5s;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location ~ /index.html {
|
||||
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
|
||||
}
|
||||
|
||||
location ~ .*\.css$|.*\.js$ {
|
||||
add_header Cache-Control 'max-age=31449600';
|
||||
}
|
||||
|
||||
location / {
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
}
|
||||
2
nise-replay-viewer/osu-parsers/.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
/**
|
||||
!/src
|
||||
180
nise-replay-viewer/osu-parsers/.eslintrc.json
Normal file
@ -0,0 +1,180 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 11
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
/* Best Practices */
|
||||
"accessor-pairs": ["error", {
|
||||
"setWithoutGet": true
|
||||
}],
|
||||
"curly": ["error", "multi-line"],
|
||||
"dot-location": ["error", "property"],
|
||||
"grouped-accessor-pairs": ["error", "getBeforeSet"],
|
||||
"eqeqeq": ["error", "always"],
|
||||
"no-case-declarations": "error",
|
||||
"no-else-return": "error",
|
||||
"no-fallthrough": "off",
|
||||
"no-floating-decimal": "error",
|
||||
"no-multi-spaces": "error",
|
||||
"no-useless-escape": "error",
|
||||
|
||||
/* Variables */
|
||||
"no-shadow": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-use-before-define": "off",
|
||||
|
||||
/* Stylistic Issues */
|
||||
"block-spacing": "error",
|
||||
"brace-style": ["error", "stroustrup"],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"comma-spacing": ["error", {
|
||||
"before": false,
|
||||
"after": true
|
||||
}],
|
||||
"comma-style": ["error", "last"],
|
||||
"eol-last": ["error", "always"],
|
||||
"indent": ["error", 2, {
|
||||
"SwitchCase": 1
|
||||
}],
|
||||
"key-spacing": ["error", {
|
||||
"beforeColon": false
|
||||
}],
|
||||
"keyword-spacing": ["error", {
|
||||
"overrides": {
|
||||
"else": { "before": false, "after": true }
|
||||
}
|
||||
}],
|
||||
"linebreak-style": "off",
|
||||
"multiline-comment-style": ["error", "starred-block"],
|
||||
"newline-per-chained-call": ["error", {
|
||||
"ignoreChainWithDepth": 2
|
||||
}],
|
||||
"no-multiple-empty-lines": ["error", {
|
||||
"max": 1,
|
||||
"maxEOF": 0
|
||||
}],
|
||||
"no-trailing-spaces": ["error", {
|
||||
"ignoreComments": true
|
||||
}],
|
||||
"no-whitespace-before-property": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"object-property-newline": ["error", {
|
||||
"allowMultiplePropertiesPerLine": false
|
||||
}],
|
||||
"padding-line-between-statements": ["error",
|
||||
{
|
||||
"blankLine": "always",
|
||||
"prev": [
|
||||
"block-like",
|
||||
"multiline-block-like",
|
||||
"multiline-expression"
|
||||
],
|
||||
"next": "*"
|
||||
},
|
||||
{
|
||||
"blankLine": "always",
|
||||
"prev": "*",
|
||||
"next": [
|
||||
"block-like",
|
||||
"multiline-block-like",
|
||||
"return"
|
||||
]
|
||||
},
|
||||
{
|
||||
"blankLine": "any",
|
||||
"prev": "case",
|
||||
"next": "case"
|
||||
},
|
||||
{
|
||||
"blankLine": "always",
|
||||
"prev": ["let", "var", "const"],
|
||||
"next": ["expression", "multiline-expression"]
|
||||
},
|
||||
{
|
||||
"blankLine": "always",
|
||||
"prev": ["expression", "multiline-expression"],
|
||||
"next": ["let", "var", "const"]
|
||||
},
|
||||
{
|
||||
"blankLine": "always",
|
||||
"prev": "*",
|
||||
"next": ["function", "class"]
|
||||
}
|
||||
],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": "off",
|
||||
"semi-spacing": ["error", {
|
||||
"before": false,
|
||||
"after": true
|
||||
}],
|
||||
"space-before-blocks": "error",
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"space-infix-ops": "error",
|
||||
"spaced-comment": "error",
|
||||
"switch-colon-spacing": "error",
|
||||
|
||||
/* ECMAScript 6 */
|
||||
"arrow-parens": "error",
|
||||
"arrow-spacing": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-useless-constructor": "off",
|
||||
"no-var": "error",
|
||||
"object-shorthand": "error",
|
||||
"prefer-arrow-callback": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"./**/*.ts",
|
||||
"./**/*.d.ts"
|
||||
],
|
||||
"rules": {
|
||||
// This rule is disabled for reverse indexing in enums
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-var-requires": "error",
|
||||
|
||||
"@typescript-eslint/semi": ["error", "always"],
|
||||
"@typescript-eslint/no-shadow": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-use-before-define": ["error", {
|
||||
"functions": false,
|
||||
"classes": false,
|
||||
"variables": true
|
||||
}],
|
||||
"@typescript-eslint/no-useless-constructor": "error",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "error",
|
||||
|
||||
"indent": "off",
|
||||
"@typescript-eslint/indent": ["error", 2]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"./**/*.js",
|
||||
"./**/*.cjs",
|
||||
"./**/*.mjs"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/semi": "off",
|
||||
"@typescript-eslint/no-shadow": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/no-useless-constructor": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"no-undef": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
9
nise-replay-viewer/osu-parsers/.npmignore
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
!/lib
|
||||
!/lib/**
|
||||
!.eslintignore
|
||||
!.eslintrc.json
|
||||
!.npmignore
|
||||
!LICENSE
|
||||
!package.json
|
||||
!README.md
|
||||
20
nise-replay-viewer/osu-parsers/LICENSE
Normal file
@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2023 Kionell
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
243
nise-replay-viewer/osu-parsers/README.md
Normal file
@ -0,0 +1,243 @@
|
||||
# osu-parsers
|
||||
[](https://www.codefactor.io/repository/github/kionell/osu-parsers)
|
||||
[](https://github.com/kionell/osu-parsers/blob/master/LICENSE)
|
||||
[](https://www.npmjs.com/package/osu-parsers)
|
||||
|
||||
|
||||
A bundle of decoders/encoders for osu! file formats based on the osu!lazer source code.
|
||||
|
||||
- Written in TypeScript.
|
||||
- Based on the osu!lazer source code.
|
||||
- Allows you to parse beatmaps of any osu! game mode.
|
||||
- Supports beatmap conversion between different game modes.
|
||||
- Works in browsers.
|
||||
|
||||
## Installation
|
||||
|
||||
Add a new dependency to your project via npm:
|
||||
|
||||
```bash
|
||||
npm install osu-parsers
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This package comes with built-in LZMA codec as it is required for replay processing.
|
||||
All classes and their typings can be found in [osu-classes](https://github.com/kionell/osu-classes) package which is a peer dependency and must be installed separately.
|
||||
|
||||
## Supported file formats
|
||||
|
||||
- [.osu](https://osu.ppy.sh/wiki/en/Client/File_formats/osu_%28file_format%29) - fully supported (decoding/encoding)
|
||||
- [.osb](https://osu.ppy.sh/wiki/en/Client/File_formats/osb_%28file_format%29) - fully supported (decoding/encoding)
|
||||
- [.osr](https://osu.ppy.sh/wiki/en/Client/File_formats/osr_%28file_format%29) - fully supported (decoding/encoding)
|
||||
|
||||
## Beatmap decoding
|
||||
|
||||
Beatmap decoder is used to read `.osu` files and convert them to the objects of plain [Beatmap](https://kionell.github.io/osu-classes/classes/Beatmap.html) type.
|
||||
Note that plain beatmap objects can't be used to get max combo, star rating and performance as they don't have ruleset specific data. To get correct beatmap data, beatmap objects should be converted using one of the supported [rulesets](https://github.com/kionell/osu-parsers#rulesets).
|
||||
|
||||
There are 4 ways to read data using this decoders:
|
||||
- via file path - async
|
||||
- via data buffer - sync
|
||||
- via string - sync
|
||||
- via array of split lines - sync
|
||||
|
||||
By default, beatmap decoder will decode both beatmap and storyboard. If you want to decode beatmap without storyboard, you can pass `false` as the second parameter to any of the methods.
|
||||
There is also a support for partial beatmap decoding which allows you to skip unnecessary sections.
|
||||
|
||||
### Example of beatmap decoding
|
||||
|
||||
```js
|
||||
import { BeatmapDecoder } from 'osu-parsers'
|
||||
|
||||
const path = 'path/to/your/file.osu';
|
||||
const data = 'osu file format v14...';
|
||||
|
||||
// This is optional and true by default.
|
||||
const shouldParseSb = true;
|
||||
|
||||
const decoder = new BeatmapDecoder();
|
||||
const beatmap1 = await decoder.decodeFromPath(path, shouldParseSb);
|
||||
|
||||
// Partial beatmap decoding without unnecessary sections.
|
||||
const beatmap2 = decoder.decodeFromString(data, {
|
||||
parseGeneral: false,
|
||||
parseEditor: false,
|
||||
parseMetadata: false,
|
||||
parseDifficulty: false,
|
||||
parseEvents: false,
|
||||
parseTimingPoints: false,
|
||||
parseHitObjects: false,
|
||||
parseStoryboard: false,
|
||||
parseColours: false,
|
||||
});
|
||||
|
||||
console.log(beatmap1) // Beatmap object.
|
||||
console.log(beatmap2) // Another Beatmap object.
|
||||
```
|
||||
|
||||
## Storyboard decoding
|
||||
|
||||
Storyboard decoder is used to read both `.osu` and `.osb` files and convert them to the [Storyboard](https://kionell.github.io/osu-classes/classes/Storyboard.html) objects.
|
||||
|
||||
As in beatmap decoder, there are 4 ways to decode your `.osu` and `.osb` files:
|
||||
- via file path - async
|
||||
- via data buffer - sync
|
||||
- via string - sync
|
||||
- via array of split lines - sync
|
||||
|
||||
### Example of storyboard decoding
|
||||
|
||||
```js
|
||||
import { StoryboardDecoder } from 'osu-parsers'
|
||||
|
||||
const pathToOsb = 'path/to/your/file.osb';
|
||||
const pathToOsu = 'path/to/your/file.osu';
|
||||
|
||||
const decoder = new StoryboardDecoder();
|
||||
|
||||
// Parse a single storyboard file (from .osb) for beatmaps that doesn't have internal storyboard (from .osu)
|
||||
const storyboard1 = await decoder.decodeFromPath(pathToOsb);
|
||||
|
||||
// Combines main storyboard (from .osu) with secondary storyboard (from .osb)
|
||||
const storyboard2 = await decoder.decodeFromPath(pathToOsu, pathToOsb);
|
||||
|
||||
console.log(storyboard1); // Storyboard object.
|
||||
console.log(storyboard2); // Another Storyboard object.
|
||||
```
|
||||
|
||||
## Score & replay decoding
|
||||
|
||||
Score decoder is used to decode `.osr` files and convert them to the [Score](https://kionell.github.io/osu-classes/classes/Score.html) objects.
|
||||
Score object contains score information and replay data of plain [Replay](https://kionell.github.io/osu-classes/classes/Replay.html) type.
|
||||
Note that all `.osr` files contain raw legacy frame data that was initially intended for osu!standard only. To get correct data, replay objects should be converted using one of the supported [rulesets](https://github.com/kionell/osu-parsers#rulesets).
|
||||
This decoder is based on external LZMA library and works asynchronously.
|
||||
|
||||
There are 2 ways to read data through this decoder:
|
||||
- via file path - async
|
||||
- via buffer - async
|
||||
|
||||
By default, score decoder will decode both score information and replay. If you want to decode score information without replay, you can pass `false` as the second parameter to any of the methods.
|
||||
|
||||
### Example of score & replay decoding
|
||||
|
||||
```js
|
||||
import { ScoreDecoder } from 'osu-parsers'
|
||||
|
||||
const path = 'path/to/your/file.osr';
|
||||
|
||||
// This is optional and true by default.
|
||||
const parseReplay = true;
|
||||
|
||||
const decoder = new ScoreDecoder();
|
||||
|
||||
const score = await decoder.decodeFromPath(path, parseReplay)
|
||||
|
||||
console.log(score.info); // ScoreInfo object.
|
||||
console.log(score.replay); // Replay object or null.
|
||||
```
|
||||
|
||||
## Encoding
|
||||
|
||||
All objects parsed by these decoders can easily be stringified and written to the files.
|
||||
Note that encoders will write object data without any changes. For example, if you try to encode beatmap with applied mods, it will write modded values!
|
||||
|
||||
When encoding is complete you can import resulting files to the game.
|
||||
|
||||
### Example of beatmap encoding
|
||||
|
||||
```js
|
||||
import { BeatmapDecoder, BeatmapEncoder } from 'osu-parsers'
|
||||
|
||||
const decodePath = 'path/to/your/file.osu';
|
||||
const shouldParseSb = true;
|
||||
|
||||
const decoder = new BeatmapDecoder();
|
||||
const encoder = new BeatmapEncoder();
|
||||
|
||||
const beatmap = await decoder.decodeFromPath(decodePath, shouldParseSb);
|
||||
|
||||
// Do your stuff with beatmap...
|
||||
|
||||
const encodePath = 'path/to/your/file.osu';
|
||||
|
||||
// Write IBeatmap object to an .osu file.
|
||||
await encoder.encodeToPath(encodePath, beatmap);
|
||||
|
||||
// You can also encode IBeatmap object to a string.
|
||||
const stringified = encoder.encodeToString(beatmap);
|
||||
```
|
||||
|
||||
### Example of storyboard encoding
|
||||
|
||||
```js
|
||||
import { StoryboardDecoder, StoryboardEncoder } from 'osu-parsers'
|
||||
|
||||
const decodePath = 'path/to/your/file.osb';
|
||||
|
||||
const decoder = new StoryboardDecoder();
|
||||
const encoder = new StoryboardEncoder();
|
||||
|
||||
const storyboard = await decoder.decodeFromPath(decodePath);
|
||||
|
||||
// Do your stuff with storyboard...
|
||||
|
||||
const encodePath = 'path/to/your/file.osb';
|
||||
|
||||
// Write Storyboard object to an .osb file.
|
||||
await encoder.encodeToPath(encodePath, storyboard);
|
||||
|
||||
// You can also encode Storyboard object to a string.
|
||||
const stringified = encoder.encodeToString(storyboard);
|
||||
```
|
||||
|
||||
### Example of score & replay encoding
|
||||
|
||||
```js
|
||||
import { ScoreDecoder, ScoreEncoder } from 'osu-parsers'
|
||||
|
||||
const decodePath = 'path/to/your/file.osr';
|
||||
|
||||
const decoder = new ScoreDecoder();
|
||||
const encoder = new ScoreEncoder();
|
||||
|
||||
const score = await decoder.decodeFromPath(decodePath);
|
||||
|
||||
// Do your stuff with score info and replay...
|
||||
|
||||
const encodePath = 'path/to/your/file.osr';
|
||||
|
||||
// Write IScore object to an .osr file.
|
||||
await encoder.encodeToPath(encodePath, score);
|
||||
|
||||
// You can also encode IScore object to a buffer.
|
||||
const buffer = await encoder.encodeToBuffer(score);
|
||||
```
|
||||
|
||||
## Rulesets
|
||||
|
||||
This library by itself doesn't provide any tools for difficulty and performance calculation!!!!
|
||||
If you are looking for something related to this, then rulesets may come in handy for you.
|
||||
Rulesets are separate libraries based on the classes from the [osu-classes](https://github.com/kionell/osu-classes.git) project. They allow you to work with gamemode specific stuff as difficulty, performance, mods and max combo calculation. Because of the shared logic between all of the rulesets they are compatible between each other. If you want, you can even write your own custom ruleset!
|
||||
The great thing about all this stuff is a beatmap and replay conversion. Any beatmap or replay can be used with any ruleset library as long as they implement the same interfaces.
|
||||
|
||||
There are 4 basic rulesets that support parsed beatmaps from this decoder:
|
||||
|
||||
- [osu-standard-stable](https://github.com/kionell/osu-standard-stable.git) - The osu!std ruleset based on the osu!lazer source code.
|
||||
- [osu-taiko-stable](https://github.com/kionell/osu-taiko-stable.git) - The osu!taiko ruleset based on the osu!lazer source code.
|
||||
- [osu-catch-stable](https://github.com/kionell/osu-catch-stable.git) - The osu!catch ruleset based on the osu!lazer source code.
|
||||
- [osu-mania-stable](https://github.com/kionell/osu-mania-stable.git) - The osu!mania ruleset based on the osu!lazer source code.
|
||||
|
||||
You can also try existing [osu-pp-calculator](https://github.com/kionell/osu-pp-calculator) package. It's a wrapper for all 4 rulesets above with a lot of extra useful stuff like score simulation and beatmap downloading.
|
||||
|
||||
## Documentation
|
||||
|
||||
Auto-generated documentation is available [here](https://kionell.github.io/osu-parsers/).
|
||||
|
||||
## Contributing
|
||||
|
||||
This project is being developed personally by me on pure enthusiasm. If you want to help with development or fix a problem, then feel free to create a new pull request. For major changes, please open an issue first to discuss what you would like to change.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](https://choosealicense.com/licenses/mit/) for details.
|
||||
3282
nise-replay-viewer/osu-parsers/lib/browser.mjs
Normal file
528
nise-replay-viewer/osu-parsers/lib/index.d.ts
vendored
Normal file
@ -0,0 +1,528 @@
|
||||
import { Beatmap, Storyboard, Score, IBeatmap, IScore, HitObject, IHasCombo, IHoldableObject, HitSample, ISlidableObject, SliderPath, ControlPointInfo, BeatmapDifficultySection, ISpinnableObject } from 'osu-classes';
|
||||
|
||||
declare enum LineType {
|
||||
FileFormat = 0,
|
||||
Section = 1,
|
||||
Empty = 2,
|
||||
Data = 3,
|
||||
Break = 4
|
||||
}
|
||||
|
||||
declare enum Section {
|
||||
General = 'General',
|
||||
Editor = 'Editor',
|
||||
Metadata = 'Metadata',
|
||||
Difficulty = 'Difficulty',
|
||||
Events = 'Events',
|
||||
TimingPoints = 'TimingPoints',
|
||||
Colours = 'Colours',
|
||||
HitObjects = 'HitObjects',
|
||||
Variables = 'Variables',
|
||||
Fonts = 'Fonts',
|
||||
CatchTheBeat = 'CatchTheBeat',
|
||||
Mania = 'Mania'
|
||||
}
|
||||
|
||||
interface IParsingOptions {
|
||||
/**
|
||||
* Whether to parse colour section or not.
|
||||
* This section is only required for beatmap & storyboard rendering stuff
|
||||
* and doesn't affect beatmap parsing and difficulty & performance calculation at all.
|
||||
*/
|
||||
parseColours?: boolean;
|
||||
}
|
||||
interface IBeatmapParsingOptions extends IParsingOptions {
|
||||
/**
|
||||
* Whether to parse general section or not.
|
||||
* This section contains very important data like beatmap mode or file format
|
||||
* and should not be omitted unless you really need to. Different beatmap file formats
|
||||
* can significantly affect beatmap parsing and difficulty & performance calculations.
|
||||
*/
|
||||
parseGeneral?: boolean;
|
||||
/**
|
||||
* Whether to parse editor section or not.
|
||||
* This section isn't required anywhere so it can be disabled safely.
|
||||
*/
|
||||
parseEditor?: boolean;
|
||||
/**
|
||||
* Whether to parse metadata section or not.
|
||||
* This section isn't required anywhere so it can be disabled safely.
|
||||
*/
|
||||
parseMetadata?: boolean;
|
||||
/**
|
||||
* Whether to parse difficulty section or not.
|
||||
* This section is required for hit object processing and difficulty & performance calculations.
|
||||
*/
|
||||
parseDifficulty?: boolean;
|
||||
/**
|
||||
* Whether to parse events section or not.
|
||||
* Events section contains information about breaks, background and storyboard.
|
||||
* Changing this will also affects storyboard parsing.
|
||||
*/
|
||||
parseEvents?: boolean;
|
||||
/**
|
||||
* Whether to parse timing point section or not.
|
||||
* Timing points are required for internal hit object processing.
|
||||
*/
|
||||
parseTimingPoints?: boolean;
|
||||
/**
|
||||
* Whether to parse hit object section or not.
|
||||
* If you don't need hit objects you can safely disable this section.
|
||||
*/
|
||||
parseHitObjects?: boolean;
|
||||
/**
|
||||
* Whether to parse storyboard or not.
|
||||
* If you don't need storyboard you can safely disable this section.
|
||||
*/
|
||||
parseStoryboard?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A basic decoder for readable file formats.
|
||||
*/
|
||||
declare abstract class Decoder {
|
||||
/**
|
||||
* Tries to read a file by its path in file system.
|
||||
* @param path A path to the file.
|
||||
* @throws If file doesn't exist or it can't be read.
|
||||
* @returns A file buffer.
|
||||
*/
|
||||
protected _getFileBuffer(path: string): Promise<Uint8Array>;
|
||||
/**
|
||||
* Tries to get last update date of a file by its path in file system.
|
||||
* @param path A path to the file.
|
||||
* @throws If file can't be read.
|
||||
* @returns Last file update date.
|
||||
*/
|
||||
protected _getFileUpdateDate(path: string): Promise<Date>;
|
||||
}
|
||||
|
||||
declare class SectionMap extends Map<Section, boolean> {
|
||||
/**
|
||||
* Current section of this map.
|
||||
*/
|
||||
currentSection: Section | null;
|
||||
/**
|
||||
* Gets the current state of the specified section.
|
||||
* @param section Section name.
|
||||
* @returns Current state of the section.
|
||||
*/
|
||||
get(section: Section): boolean;
|
||||
/**
|
||||
* Sets the state of the specified section.
|
||||
* @param section Section name.
|
||||
* @param state State of the section.
|
||||
* @returns Reference to this section map.
|
||||
*/
|
||||
set(section: Section, state?: boolean): this;
|
||||
/**
|
||||
* Resets all section states to enabled and removes current section.
|
||||
* @returns Reference to this section map.
|
||||
*/
|
||||
reset(): this;
|
||||
get hasEnabledSections(): boolean;
|
||||
/**
|
||||
* Check if current section is enabled and should be parsed.
|
||||
* Unknown sections are disabled by default.
|
||||
* @returns If this section is enabled.
|
||||
*/
|
||||
get isSectionEnabled(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A decoder for human-readable file formats that consist of sections.
|
||||
*/
|
||||
declare abstract class SectionDecoder<T> extends Decoder {
|
||||
/**
|
||||
* Current data lines.
|
||||
*/
|
||||
protected _lines: string[] | null;
|
||||
/**
|
||||
* Section map of this decoder.
|
||||
*/
|
||||
protected _sectionMap: SectionMap;
|
||||
protected _getLines(data: any[]): string[];
|
||||
protected _parseLine(line: string, output: T): LineType;
|
||||
protected _parseSectionData(line: string, output: T): void;
|
||||
protected _preprocessLine(line: string): string;
|
||||
protected _shouldSkipLine(line: string): boolean;
|
||||
protected _stripComments(line: string): string;
|
||||
protected _reset(): void;
|
||||
/**
|
||||
* Sets current enabled sections.
|
||||
* All known sections are enabled by default.
|
||||
* @param options Parsing options.
|
||||
*/
|
||||
protected _setEnabledSections(options?: IParsingOptions): void;
|
||||
}
|
||||
|
||||
declare type BufferLike = ArrayBufferLike | Uint8Array;
|
||||
|
||||
/**
|
||||
* A beatmap decoder.
|
||||
*/
|
||||
declare class BeatmapDecoder extends SectionDecoder<Beatmap> {
|
||||
/**
|
||||
* An offset which needs to be applied to old beatmaps (v4 and lower)
|
||||
* to correct timing changes that were applied at a game client level.
|
||||
*/
|
||||
static readonly EARLY_VERSION_TIMING_OFFSET = 24;
|
||||
/**
|
||||
* Current offset for all time values.
|
||||
*/
|
||||
protected _offset: number;
|
||||
/**
|
||||
* Current storyboard lines.
|
||||
*/
|
||||
protected _sbLines: string[] | null;
|
||||
/**
|
||||
* Performs beatmap decoding from the specified .osu file.
|
||||
* @param path A path to the .osu file.
|
||||
* @param options Beatmap parsing options.
|
||||
* Setting this to boolean will only affect storyboard parsing.
|
||||
* All sections that weren't specified will be enabled by default.
|
||||
* @throws If file doesn't exist or can't be decoded.
|
||||
* @returns A decoded beatmap.
|
||||
*/
|
||||
decodeFromPath(path: string, options?: boolean | IBeatmapParsingOptions): Promise<Beatmap>;
|
||||
/**
|
||||
* Performs beatmap decoding from a data buffer.
|
||||
* @param data The buffer with beatmap data.
|
||||
* @param options Beatmap parsing options.
|
||||
* Setting this to boolean will only affect storyboard parsing.
|
||||
* All sections that weren't specified will be enabled by default.
|
||||
* @throws If beatmap data can't be decoded.
|
||||
* @returns A decoded beatmap.
|
||||
*/
|
||||
decodeFromBuffer(data: BufferLike, options?: boolean | IBeatmapParsingOptions): Beatmap;
|
||||
/**
|
||||
* Performs beatmap decoding from a string.
|
||||
* @param str The string with beatmap data.
|
||||
* @param options Beatmap parsing options.
|
||||
* Setting this to boolean will only affect storyboard parsing.
|
||||
* All sections that weren't specified will be enabled by default.
|
||||
* @throws If beatmap data can't be decoded.
|
||||
* @returns A decoded beatmap.
|
||||
*/
|
||||
decodeFromString(str: string, options?: boolean | IBeatmapParsingOptions): Beatmap;
|
||||
/**
|
||||
* Performs beatmap decoding from a string array.
|
||||
* @param data The array of split lines.
|
||||
* @param options Beatmap parsing options.
|
||||
* Setting this to boolean will only affect storyboard parsing.
|
||||
* @throws If beatmap data can't be decoded.
|
||||
* @returns A decoded beatmap.
|
||||
*/
|
||||
decodeFromLines(data: string[], options?: boolean | IBeatmapParsingOptions): Beatmap;
|
||||
protected _parseLine(line: string, beatmap: Beatmap): LineType;
|
||||
protected _parseSectionData(line: string, beatmap: Beatmap): void;
|
||||
/**
|
||||
* Sets current enabled sections.
|
||||
* All known sections are enabled by default.
|
||||
* @param options Parsing options.
|
||||
*/
|
||||
protected _setEnabledSections(options?: IBeatmapParsingOptions): void;
|
||||
protected _shouldParseStoryboard(options?: boolean | IBeatmapParsingOptions): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A storyboard decoder.
|
||||
*/
|
||||
declare class StoryboardDecoder extends SectionDecoder<Storyboard> {
|
||||
/**
|
||||
* Current section name.
|
||||
*/
|
||||
private _variables;
|
||||
/**
|
||||
* Performs storyboard decoding from the specified `.osu` or `.osb` file.
|
||||
* If two paths were specified, storyboard decoder will try to combine storyboards.
|
||||
*
|
||||
* NOTE: Commands from the `.osb` file take precedence over those
|
||||
* from the `.osu` file within the layers, as if the commands
|
||||
* from the `.osb` were appended to the end of the `.osu` commands.
|
||||
* @param firstPath The path to the main storyboard (`.osu` or `.osb` file).
|
||||
* @param secondPath The path to the secondary storyboard (`.osb` file).
|
||||
* @throws If file doesn't exist or can't be decoded.
|
||||
* @returns A decoded storyboard.
|
||||
*/
|
||||
decodeFromPath(firstPath: string, secondPath?: string): Promise<Storyboard>;
|
||||
/**
|
||||
* Performs storyboard decoding from a data buffer.
|
||||
* If two data buffers were specified, storyboard decoder will try to combine storyboards.
|
||||
*
|
||||
* NOTE: Commands from the `.osb` file take precedence over those
|
||||
* from the `.osu` file within the layers, as if the commands
|
||||
* from the `.osb` were appended to the end of the `.osu` commands.
|
||||
* @param firstBuffer The buffer with the main storyboard data (from `.osu` or `.osb` file).
|
||||
* @param secondBuffer The buffer with the secondary storyboard data (from `.osb` file).
|
||||
* @throws If storyboard data can't be decoded.
|
||||
* @returns A decoded storyboard.
|
||||
*/
|
||||
decodeFromBuffer(firstBuffer: BufferLike, secondBuffer?: BufferLike): Storyboard;
|
||||
/**
|
||||
* Performs storyboard decoding from a string.
|
||||
* If two strings were specified, storyboard decoder will try to combine storyboards.
|
||||
*
|
||||
* NOTE: Commands from the `.osb` file take precedence over those
|
||||
* from the `.osu` file within the layers, as if the commands
|
||||
* from the `.osb` were appended to the end of the `.osu` commands.
|
||||
* @param firstString The string with the main storyboard data (from `.osu` or `.osb` file).
|
||||
* @param secondString The string with the secondary storyboard data (from `.osb` file).
|
||||
* @throws If storyboard data can't be decoded.
|
||||
* @returns A decoded storyboard.
|
||||
*/
|
||||
decodeFromString(firstString: string, secondString?: string): Storyboard;
|
||||
/**
|
||||
* Performs storyboard decoding from a string array.
|
||||
* If two string arrays were specified, storyboard decoder will try to combine storyboards.
|
||||
*
|
||||
* NOTE: Commands from the `.osb` file take precedence over those
|
||||
* from the `.osu` file within the layers, as if the commands
|
||||
* from the `.osb` were appended to the end of the `.osu` commands.
|
||||
* @param firstData The string array with the main storyboard data (from `.osu` or `.osb` file).
|
||||
* @param secondData The string array with the secondary storyboard data (from `.osb` file).
|
||||
* @throws If storyboard data can't be decoded.
|
||||
* @returns A decoded storyboard.
|
||||
*/
|
||||
decodeFromLines(firstData: string[], secondData?: string[]): Storyboard;
|
||||
protected _parseLine(line: string, storyboard: Storyboard): LineType;
|
||||
protected _parseSectionData(line: string, storyboard: Storyboard): void;
|
||||
/**
|
||||
* Sets current enabled sections.
|
||||
* All known sections are enabled by default.
|
||||
*/
|
||||
protected _setEnabledSections(): void;
|
||||
protected _preprocessLine(line: string): string;
|
||||
protected _reset(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A score decoder.
|
||||
*/
|
||||
declare class ScoreDecoder extends Decoder {
|
||||
/**
|
||||
* Performs score decoding from the specified .osr file.
|
||||
* @param path A path to the .osr file.
|
||||
* @param parseReplay Should replay be parsed?
|
||||
* @throws If file doesn't exist or can't be decoded.
|
||||
* @returns A decoded score.
|
||||
*/
|
||||
decodeFromPath(path: string, parseReplay?: boolean): Promise<Score>;
|
||||
/**
|
||||
* Performs score decoding from a buffer.
|
||||
* @param buffer The buffer with score data.
|
||||
* @param parseReplay Should replay be parsed?
|
||||
* @returns A decoded score.
|
||||
*/
|
||||
decodeFromBuffer(buffer: BufferLike, parseReplay?: boolean): Promise<Score>;
|
||||
private _parseScoreId;
|
||||
}
|
||||
|
||||
/**
|
||||
* A beatmap encoder.
|
||||
*/
|
||||
declare class BeatmapEncoder {
|
||||
/**
|
||||
* First playable lazer version.
|
||||
*/
|
||||
static readonly FIRST_LAZER_VERSION = 128;
|
||||
/**
|
||||
* Performs beatmap encoding to the specified path.
|
||||
* @param path The path for writing the .osu file.
|
||||
* @param beatmap The beatmap for encoding.
|
||||
* @throws If beatmap can't be encoded or file can't be written.
|
||||
*/
|
||||
encodeToPath(path: string, beatmap?: IBeatmap): Promise<void>;
|
||||
/**
|
||||
* Performs beatmap encoding to a string.
|
||||
* @param beatmap The beatmap for encoding.
|
||||
* @returns The string with encoded beatmap data.
|
||||
*/
|
||||
encodeToString(beatmap?: IBeatmap): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A storyboard encoder.
|
||||
*/
|
||||
declare class StoryboardEncoder {
|
||||
/**
|
||||
* Performs storyboard encoding to the specified path.
|
||||
* @param path The path for writing the .osb file.
|
||||
* @param storyboard The storyboard for encoding.
|
||||
* @throws If storyboard can't be encoded or file can't be written.
|
||||
*/
|
||||
encodeToPath(path: string, storyboard?: Storyboard): Promise<void>;
|
||||
/**
|
||||
* Performs storyboard encoding to a string.
|
||||
* @param storyboard The storyboard for encoding.
|
||||
* @returns The string with encoded storyboard data.
|
||||
*/
|
||||
encodeToString(storyboard?: Storyboard): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A score encoder.
|
||||
*/
|
||||
declare class ScoreEncoder {
|
||||
/**
|
||||
* Default game version used if replay is not available.
|
||||
* It's just the last available osu!lazer version at the moment.
|
||||
*/
|
||||
static DEFAULT_GAME_VERSION: number;
|
||||
/**
|
||||
* Performs score & replay encoding to the specified path.
|
||||
* @param path The path for writing the .osr file.
|
||||
* @param score The score for encoding.
|
||||
* @param beatmap The beatmap of the replay.
|
||||
* It is required if replay contains non-legacy frames.
|
||||
* @throws If score can't be encoded
|
||||
* @throws If beatmap wasn't provided for non-legacy replay.
|
||||
* @throws If score can't be encoded or file can't be written.
|
||||
*/
|
||||
encodeToPath(path: string, score?: IScore, beatmap?: IBeatmap): Promise<void>;
|
||||
/**
|
||||
* Performs score encoding to a buffer.
|
||||
* @param score The score for encoding.
|
||||
* @param beatmap The beatmap of the replay.
|
||||
* It is required if replay contains non-legacy frames.
|
||||
* @throws If beatmap wasn't provided for non-legacy replay.
|
||||
* @returns The buffer with encoded score & replay data.
|
||||
*/
|
||||
encodeToBuffer(score?: IScore, beatmap?: IBeatmap): Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A hittable object.
|
||||
*/
|
||||
declare class HittableObject extends HitObject implements IHasCombo {
|
||||
isNewCombo: boolean;
|
||||
comboOffset: number;
|
||||
/**
|
||||
* Creates a copy of this parsed hit.
|
||||
* Non-primitive properties will be copied via their own clone() method.
|
||||
* @returns A copied parsed slider.
|
||||
*/
|
||||
clone(): this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A holdable object.
|
||||
*/
|
||||
declare class HoldableObject extends HitObject implements IHoldableObject {
|
||||
/**
|
||||
* The time at which the holdable object ends.
|
||||
*/
|
||||
endTime: number;
|
||||
/**
|
||||
* The samples to be played when each node of the holdable object is hit.
|
||||
* 0: The first node.
|
||||
* 1: The first repeat.
|
||||
* 2: The second repeat.
|
||||
* ...
|
||||
* n-1: The last repeat.
|
||||
* n: The last node.
|
||||
*/
|
||||
nodeSamples: HitSample[][];
|
||||
/**
|
||||
* The duration of the holdable object.
|
||||
*/
|
||||
get duration(): number;
|
||||
/**
|
||||
* Creates a copy of this holdable object.
|
||||
* Non-primitive properties will be copied via their own clone() method.
|
||||
* @returns A copied holdable object.
|
||||
*/
|
||||
clone(): this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A parsed slidable object.
|
||||
*/
|
||||
declare class SlidableObject extends HitObject implements ISlidableObject, IHasCombo {
|
||||
/**
|
||||
* Scoring distance with a speed-adjusted beat length of 1 second
|
||||
* (ie. the speed slider balls move through their track).
|
||||
*/
|
||||
static BASE_SCORING_DISTANCE: number;
|
||||
/**
|
||||
* The duration of this slidable object.
|
||||
*/
|
||||
get duration(): number;
|
||||
/**
|
||||
* The time at which the slidable object ends.
|
||||
*/
|
||||
get endTime(): number;
|
||||
/**
|
||||
* The amount of times the length of this slidable object spans.
|
||||
*/
|
||||
get spans(): number;
|
||||
set spans(value: number);
|
||||
/**
|
||||
* The duration of a single span of this slidable object.
|
||||
*/
|
||||
get spanDuration(): number;
|
||||
/**
|
||||
* The positional length of a slidable object.
|
||||
*/
|
||||
get distance(): number;
|
||||
set distance(value: number);
|
||||
/**
|
||||
* The amount of times a slidable object repeats.
|
||||
*/
|
||||
repeats: number;
|
||||
/**
|
||||
* Velocity of this slidable object.
|
||||
*/
|
||||
velocity: number;
|
||||
/**
|
||||
* The curve of a slidable object.
|
||||
*/
|
||||
path: SliderPath;
|
||||
/**
|
||||
* The last tick offset of slidable objects in osu!stable.
|
||||
*/
|
||||
legacyLastTickOffset: number;
|
||||
/**
|
||||
* The samples to be played when each node of the slidable object is hit.
|
||||
* 0: The first node.
|
||||
* 1: The first repeat.
|
||||
* 2: The second repeat.
|
||||
* ...
|
||||
* n-1: The last repeat.
|
||||
* n: The last node.
|
||||
*/
|
||||
nodeSamples: HitSample[][];
|
||||
isNewCombo: boolean;
|
||||
comboOffset: number;
|
||||
applyDefaultsToSelf(controlPoints: ControlPointInfo, difficulty: BeatmapDifficultySection): void;
|
||||
/**
|
||||
* Creates a copy of this parsed slider.
|
||||
* Non-primitive properties will be copied via their own clone() method.
|
||||
* @returns A copied parsed slider.
|
||||
*/
|
||||
clone(): this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A parsed spinnable object.
|
||||
*/
|
||||
declare class SpinnableObject extends HitObject implements ISpinnableObject, IHasCombo {
|
||||
/**
|
||||
* The time at which the spinnable object ends.
|
||||
*/
|
||||
endTime: number;
|
||||
isNewCombo: boolean;
|
||||
comboOffset: number;
|
||||
/**
|
||||
* The duration of this spinnable object.
|
||||
*/
|
||||
get duration(): number;
|
||||
/**
|
||||
* Creates a copy of this parsed spinner.
|
||||
* Non-primitive properties will be copied via their own clone() method.
|
||||
* @returns A copied parsed spinner.
|
||||
*/
|
||||
clone(): this;
|
||||
}
|
||||
|
||||
export { BeatmapDecoder, BeatmapEncoder, HittableObject, HoldableObject, ScoreDecoder, ScoreEncoder, SlidableObject, SpinnableObject, StoryboardDecoder, StoryboardEncoder };
|
||||
65
nise-replay-viewer/osu-parsers/package.json
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "osu-parsers",
|
||||
"version": "4.1.3",
|
||||
"description": "A bundle of parsers for osu! file formats based on the osu!lazer source code.",
|
||||
"exports": {
|
||||
"node": {
|
||||
"import": "./lib/node.mjs",
|
||||
"require": "./lib/node.cjs"
|
||||
},
|
||||
"import": "./lib/browser.mjs"
|
||||
},
|
||||
"types": "./lib/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "rollup -c && npm run format",
|
||||
"test": "jest --verbose",
|
||||
"fix": "eslint --fix ./src",
|
||||
"format": "eslint --fix ./lib/** --no-ignore",
|
||||
"prepublishOnly": "npm run build",
|
||||
"docs": "npx typedoc"
|
||||
},
|
||||
"keywords": [
|
||||
"osu",
|
||||
"beatmap",
|
||||
"storyboard",
|
||||
"score",
|
||||
"replay",
|
||||
"parser",
|
||||
"osu!std",
|
||||
"osu!taiko",
|
||||
"osu!mania",
|
||||
"osu!catch",
|
||||
".osu",
|
||||
".osb",
|
||||
".osr"
|
||||
],
|
||||
"author": "Kionell",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kionell/osu-parsers"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^21.0.1",
|
||||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@types/jest": "^26.0.22",
|
||||
"@types/node": "^14.18.12",
|
||||
"@typescript-eslint/eslint-plugin": "^4.20.0",
|
||||
"@typescript-eslint/parser": "^4.21.0",
|
||||
"eslint": "^7.23.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"jest": "^26.6.3",
|
||||
"rollup": "^2.44.0",
|
||||
"rollup-plugin-dts": "^3.0.1",
|
||||
"rollup-plugin-node-externals": "^3.1.2",
|
||||
"typedoc": "^0.22.10",
|
||||
"typedoc-plugin-missing-exports": "^1.0.0",
|
||||
"typescript": "^4.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"lzma-js-simple-v2": "^1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"osu-classes": "^3.0.0"
|
||||
}
|
||||
}
|
||||
3898
nise-replay-viewer/package-lock.json
generated
Normal file
52
nise-replay-viewer/package.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "sample-p5-editor",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"tsc": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/events": "^3.0.3",
|
||||
"@types/p5": "^0.9.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"typescript": "^4.2.3",
|
||||
"vite": "^4.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@osujs/core": "^0.0.6",
|
||||
"@osujs/math": "^0.0.4",
|
||||
"@radix-ui/react-context-menu": "^2.1.5",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-menubar": "^1.0.4",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"events": "^3.3.0",
|
||||
"lucide-react": "^0.292.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"osu-classes": "^3.0.1",
|
||||
"osu-parsers": "^4.1.6",
|
||||
"osu-standard-stable": "^5.0.0",
|
||||
"p5": "^1.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-router-dom": "^6.22.2",
|
||||
"recharts": "^2.10.4",
|
||||
"sonner": "^1.3.1",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"ts-md5": "^1.3.1",
|
||||
"zustand": "^4.4.1"
|
||||
}
|
||||
}
|
||||
6
nise-replay-viewer/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
5
nise-replay-viewer/public/ctrl.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="35" height="26" viewBox="0 0 35 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="2.12109" width="35" height="23.3333" rx="4.61029" fill="#222222"/>
|
||||
<rect width="35" height="21.2121" rx="4.61029" fill="#373737"/>
|
||||
<path d="M10.5078 13.6565C9.77016 13.6565 9.11037 13.509 8.52845 13.214C7.95473 12.9189 7.50394 12.5132 7.1761 11.9968C6.84826 11.4723 6.68433 10.874 6.68433 10.2019C6.68433 9.52982 6.84826 8.9356 7.1761 8.41925C7.50394 7.9029 7.95473 7.50129 8.52845 7.21443C9.11037 6.91937 9.77016 6.77184 10.5078 6.77184C11.2618 6.77184 11.9134 6.93166 12.4626 7.25131C13.0117 7.57096 13.401 8.02174 13.6305 8.60366L11.8233 9.52572C11.6676 9.21427 11.4749 8.98888 11.2455 8.84955C11.016 8.70202 10.766 8.62825 10.4955 8.62825C10.225 8.62825 9.97916 8.68972 9.75786 8.81266C9.53657 8.9356 9.36035 9.11592 9.22922 9.3536C9.10628 9.58309 9.04481 9.86586 9.04481 10.2019C9.04481 10.5461 9.10628 10.8371 9.22922 11.0748C9.36035 11.3125 9.53657 11.4928 9.75786 11.6157C9.97916 11.7387 10.225 11.8001 10.4955 11.8001C10.766 11.8001 11.016 11.7305 11.2455 11.5911C11.4749 11.4436 11.6676 11.2141 11.8233 10.9027L13.6305 11.8247C13.401 12.4066 13.0117 12.8574 12.4626 13.1771C11.9134 13.4967 11.2618 13.6565 10.5078 13.6565ZM17.5933 13.6565C16.7409 13.6565 16.077 13.4475 15.6016 13.0295C15.1262 12.6033 14.8885 11.9641 14.8885 11.1117V5.3949H17.2244V11.0871C17.2244 11.3248 17.29 11.5133 17.4211 11.6526C17.5523 11.7837 17.7203 11.8493 17.9252 11.8493C18.2039 11.8493 18.4415 11.7796 18.6383 11.6403L19.2161 13.2754C19.0194 13.4066 18.7776 13.5008 18.4907 13.5582C18.2039 13.6238 17.9047 13.6565 17.5933 13.6565ZM13.9173 8.87413V7.12837H18.7489V8.87413H13.9173ZM20.1168 13.5459V6.88249H22.342V8.82496L22.0101 8.27172C22.2068 7.77176 22.5265 7.39884 22.969 7.15296C23.4116 6.89888 23.9485 6.77184 24.5796 6.77184V8.87413C24.473 8.85774 24.3788 8.84955 24.2968 8.84955C24.223 8.84135 24.1411 8.83725 24.0509 8.83725C23.5756 8.83725 23.1903 8.96839 22.8953 9.23066C22.6002 9.48474 22.4527 9.90274 22.4527 10.4847V13.5459H20.1168ZM25.5435 13.5459V4.42366H27.8794V13.5459H25.5435Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
BIN
nise-replay-viewer/public/cursor.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
nise-replay-viewer/public/cursortrail.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
nise-replay-viewer/public/default0.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
nise-replay-viewer/public/default1.png
Normal file
|
After Width: | Height: | Size: 624 B |
BIN
nise-replay-viewer/public/default2.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
nise-replay-viewer/public/default3.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
nise-replay-viewer/public/default4.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
nise-replay-viewer/public/default5.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
nise-replay-viewer/public/default6.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
nise-replay-viewer/public/default7.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
nise-replay-viewer/public/default8.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
nise-replay-viewer/public/default9.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
5
nise-replay-viewer/public/ekey.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.411133" y="3.20605" width="24.5882" height="21.5147" rx="4.61029" fill="#222222"/>
|
||||
<rect x="0.411133" y="0.970703" width="24.5882" height="19.9779" rx="4.61029" fill="#373737"/>
|
||||
<path d="M12.5329 14.0228C11.746 14.0228 11.0576 13.8752 10.4674 13.5802C9.88553 13.2769 9.43064 12.8671 9.1028 12.3508C8.78315 11.8262 8.62333 11.232 8.62333 10.5681C8.62333 9.90423 8.77906 9.31411 9.09051 8.79776C9.41015 8.27321 9.84864 7.8675 10.406 7.58064C10.9633 7.28558 11.5903 7.13805 12.287 7.13805C12.9427 7.13805 13.541 7.27329 14.0819 7.54376C14.6229 7.80603 15.0532 8.19534 15.3728 8.7117C15.6924 9.22805 15.8523 9.85505 15.8523 10.5927C15.8523 10.6747 15.8482 10.7689 15.84 10.8755C15.8318 10.982 15.8236 11.0804 15.8154 11.1705H10.5412V9.94111H14.5737L13.6885 10.2853C13.6967 9.98209 13.6393 9.71981 13.5164 9.49852C13.4016 9.27723 13.2377 9.10511 13.0246 8.98217C12.8197 8.85923 12.5779 8.79776 12.2993 8.79776C12.0206 8.79776 11.7747 8.85923 11.5616 8.98217C11.3567 9.10511 11.1969 9.28132 11.0822 9.51082C10.9674 9.73211 10.91 9.99438 10.91 10.2976V10.6542C10.91 10.982 10.9756 11.2648 11.1067 11.5025C11.2461 11.7401 11.4428 11.9246 11.6969 12.0557C11.9509 12.1786 12.2542 12.2401 12.6066 12.2401C12.9345 12.2401 13.2131 12.195 13.4426 12.1049C13.6803 12.0065 13.9139 11.859 14.1434 11.6623L15.3728 12.9409C15.0532 13.2933 14.6597 13.5638 14.1926 13.7523C13.7254 13.9326 13.1722 14.0228 12.5329 14.0228Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
nise-replay-viewer/public/fav.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
nise-replay-viewer/public/gitlogo.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
nise-replay-viewer/public/hitcircle.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
nise-replay-viewer/public/hitcircleoverlay.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
nise-replay-viewer/public/icon.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
nise-replay-viewer/public/mod_dt.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
nise-replay-viewer/public/mod_ez.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
nise-replay-viewer/public/mod_fl.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
nise-replay-viewer/public/mod_hd.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
nise-replay-viewer/public/mod_hr.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
nise-replay-viewer/public/mod_ht.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
nise-replay-viewer/public/mod_nc.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
nise-replay-viewer/public/mod_nf.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
nise-replay-viewer/public/mod_nm.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
nise-replay-viewer/public/mod_pf.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
nise-replay-viewer/public/mod_sd.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
6
nise-replay-viewer/public/mouse.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="16" height="26" viewBox="0 0 16 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.52637 7.48637V9.63586C6.52637 10.4498 7.18613 11.1095 8.00004 11.1095C8.81395 11.1095 9.47371 10.4498 9.47371 9.63586V7.48637C9.47371 6.67246 8.81395 6.0127 8.00004 6.0127C7.18613 6.0127 6.52637 6.67246 6.52637 7.48637Z" fill="white"/>
|
||||
<path d="M5.05306 7.4854C5.05306 6.11459 5.9938 4.95933 7.26357 4.63134V0.442383C3.19678 0.815565 0.000976562 4.24509 0.000976562 8.40757V8.52409H5.05311V7.4854H5.05306Z" fill="#373737"/>
|
||||
<path d="M8.7373 0.442383V4.63129C10.0071 4.95933 10.9478 6.11454 10.9478 7.48535V8.52409H15.9999V8.40757C15.9999 4.24509 12.8041 0.815565 8.7373 0.442383V0.442383Z" fill="#373737"/>
|
||||
<path d="M8.0004 12.5825C6.49819 12.5825 5.25574 11.4525 5.0762 9.99805H0.000976562V17.5595C0.000976562 21.9704 3.58951 25.559 8.00045 25.559C12.4114 25.559 15.9999 21.9705 15.9999 17.5595V9.99805H10.9246C10.7451 11.4525 9.50266 12.5825 8.0004 12.5825Z" fill="#373737"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 993 B |
5
nise-replay-viewer/public/qkey.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="3.20605" width="24.5882" height="21.5147" rx="4.61029" fill="#222222"/>
|
||||
<rect y="0.970703" width="24.5882" height="19.9779" rx="4.61029" fill="#373737"/>
|
||||
<path d="M11.6513 14.0228C11.053 14.0228 10.5038 13.8834 10.0039 13.6048C9.50392 13.3261 9.10641 12.9286 8.81135 12.4122C8.52449 11.8959 8.38106 11.2853 8.38106 10.5804C8.38106 9.87554 8.52449 9.26903 8.81135 8.76087C9.10641 8.24452 9.50392 7.84701 10.0039 7.56834C10.5038 7.28148 11.053 7.13805 11.6513 7.13805C12.2578 7.13805 12.7578 7.26099 13.1512 7.50687C13.5446 7.75276 13.8396 8.12978 14.0364 8.63793C14.2331 9.13789 14.3314 9.78538 14.3314 10.5804C14.3314 11.3672 14.2249 12.0147 14.0118 12.5229C13.8069 13.0228 13.5036 13.3999 13.102 13.6539C12.7086 13.8998 12.225 14.0228 11.6513 14.0228ZM12.1431 12.1663C12.4053 12.1663 12.6389 12.1049 12.8438 11.9819C13.0569 11.859 13.2249 11.6787 13.3479 11.441C13.479 11.2033 13.5446 10.9164 13.5446 10.5804C13.5446 10.2444 13.479 9.9575 13.3479 9.71981C13.2249 9.48213 13.0569 9.30182 12.8438 9.17887C12.6389 9.05593 12.4053 8.99446 12.1431 8.99446C11.8808 8.99446 11.6431 9.05593 11.43 9.17887C11.2251 9.30182 11.0571 9.48213 10.9259 9.71981C10.803 9.9575 10.7415 10.2444 10.7415 10.5804C10.7415 10.9164 10.803 11.2033 10.9259 11.441C11.0571 11.6787 11.2251 11.859 11.43 11.9819C11.6431 12.1049 11.8808 12.1663 12.1431 12.1663ZM13.5077 16.2972V12.7933L13.6306 10.5804L13.6184 8.37976V7.2487H15.8436V16.2972H13.5077Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
nise-replay-viewer/public/radix.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
7
nise-replay-viewer/public/scroll.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="28" height="26" viewBox="0 0 28 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.52637 7.48637V9.63586C6.52637 10.4498 7.18613 11.1095 8.00004 11.1095C8.81395 11.1095 9.47371 10.4498 9.47371 9.63586V7.48637C9.47371 6.67246 8.81395 6.0127 8.00004 6.0127C7.18613 6.0127 6.52637 6.67246 6.52637 7.48637Z" fill="white"/>
|
||||
<path d="M5.05306 7.4854C5.05306 6.11459 5.9938 4.95933 7.26357 4.63134V0.442383C3.19678 0.815565 0.000976562 4.24509 0.000976562 8.40757V8.52409H5.05311V7.4854H5.05306Z" fill="#373737"/>
|
||||
<path d="M8.7373 0.442383V4.63129C10.0071 4.95933 10.9478 6.11454 10.9478 7.48535V8.52409H15.9999V8.40757C15.9999 4.24509 12.8041 0.815565 8.7373 0.442383V0.442383Z" fill="#373737"/>
|
||||
<path d="M8.0004 12.5825C6.49819 12.5825 5.25574 11.4525 5.0762 9.99805H0.000976562V17.5595C0.000976562 21.9704 3.58951 25.559 8.00045 25.559C12.4114 25.559 15.9999 21.9705 15.9999 17.5595V9.99805H10.9246C10.7451 11.4525 9.50266 12.5825 8.0004 12.5825Z" fill="#373737"/>
|
||||
<path d="M23.6663 5V21M23.6663 5L20.9997 7.66667M23.6663 5L26.333 7.66667M23.6663 21L20.9997 18.3333M23.6663 21L26.333 18.3333" stroke="white" stroke-width="1.52381" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
nise-replay-viewer/public/sliderb0.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
nise-replay-viewer/public/sliderfollowcircle.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
5
nise-replay-viewer/public/space.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
1
nise-replay-viewer/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
11
nise-replay-viewer/src/decorators/hook.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Events, hook } from "@/hooks";
|
||||
|
||||
export function Hook(event: Events): any {
|
||||
return function (
|
||||
_target: any,
|
||||
_propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
) {
|
||||
hook({ event, callback: descriptor.value });
|
||||
};
|
||||
}
|
||||
14
nise-replay-viewer/src/decorators/singleton.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export function Singleton<T extends new (...args: any[]) => any>(ctr: T): T {
|
||||
let instance: T;
|
||||
|
||||
return class {
|
||||
constructor(...args: any[]) {
|
||||
if (instance) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
instance = new ctr(...args);
|
||||
return instance;
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
76
nise-replay-viewer/src/globals.css
Normal file
@ -0,0 +1,76 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
38
nise-replay-viewer/src/hooks.ts
Normal file
@ -0,0 +1,38 @@
|
||||
export interface Hook<T> {
|
||||
event: T;
|
||||
callback: (...args: any) => boolean | void;
|
||||
}
|
||||
|
||||
export enum Events {
|
||||
setup,
|
||||
draw,
|
||||
preload,
|
||||
resize,
|
||||
mousePressed,
|
||||
mouseDragged,
|
||||
mouseReleased,
|
||||
mouseWheel,
|
||||
keyPressed,
|
||||
keyReleased,
|
||||
}
|
||||
|
||||
const hooks: Map<any, Hook<Events>[]> = new Map();
|
||||
|
||||
export function hook({ event, callback }: Hook<any>) {
|
||||
if (!hooks.get(event)) {
|
||||
hooks.set(event, []);
|
||||
}
|
||||
hooks.set(event, [...hooks.get(event)!, { event, callback }]);
|
||||
}
|
||||
|
||||
export function hookable(event: Events) {
|
||||
return (...args: any) => {
|
||||
const hookList = hooks.get(event);
|
||||
|
||||
if (!hookList?.length) return;
|
||||
|
||||
for (const hook of hookList) {
|
||||
hook.callback(...args);
|
||||
}
|
||||
};
|
||||
}
|
||||
63
nise-replay-viewer/src/hooks/canvasControls.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { Hook } from "@/decorators/hook";
|
||||
import { Events } from "@/hooks";
|
||||
import { p } from "@/utils";
|
||||
import p5 from "p5";
|
||||
|
||||
export let canvasDragging: boolean;
|
||||
export let canvasDragStart: p5.Vector;
|
||||
export let canvasMultiplier: number;
|
||||
export let canvasTranslation: p5.Vector;
|
||||
|
||||
export class CanvasControlHooks {
|
||||
@Hook(Events.setup)
|
||||
static setupCanvasControls() {
|
||||
canvasDragStart = p.createVector(0, 0);
|
||||
canvasDragging = false;
|
||||
canvasMultiplier = p.windowHeight / 384;
|
||||
canvasTranslation = p.createVector(0, 0);
|
||||
}
|
||||
|
||||
@Hook(Events.mousePressed)
|
||||
static mousePressed() {
|
||||
if (p.mouseButton === p.CENTER) {
|
||||
canvasDragging = true;
|
||||
canvasDragStart = p.createVector(p.mouseX, p.mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
@Hook(Events.mouseDragged)
|
||||
static mouseDragged() {
|
||||
if (canvasDragging) {
|
||||
const mousePos = p.createVector(p.mouseX, p.mouseY);
|
||||
const delta = mousePos.copy().sub(canvasDragStart).div(canvasMultiplier);
|
||||
canvasTranslation.add(delta);
|
||||
canvasDragStart = mousePos;
|
||||
}
|
||||
}
|
||||
|
||||
@Hook(Events.mouseReleased)
|
||||
static mouseReleased() {
|
||||
canvasDragging = false;
|
||||
}
|
||||
|
||||
@Hook(Events.mouseWheel)
|
||||
static mouseWheel(event: any) {
|
||||
if (!p.keyIsDown(17)) {
|
||||
return;
|
||||
}
|
||||
const zoom = event.deltaY > 0 ? 100 : -100;
|
||||
const oldScale = canvasMultiplier;
|
||||
const multiplierChange = zoom * 0.0008 * oldScale;
|
||||
canvasMultiplier -= multiplierChange;
|
||||
|
||||
const newScale = canvasMultiplier;
|
||||
const mousePos = p.createVector(p.mouseX, p.mouseY);
|
||||
const mousePosScaled = mousePos.copy().div(oldScale);
|
||||
const mousePosScaledNew = mousePos.copy().div(newScale);
|
||||
const delta = mousePosScaledNew.copy().sub(mousePosScaled);
|
||||
canvasTranslation.add(delta);
|
||||
event.preventDefault();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
46
nise-replay-viewer/src/hooks/osuControls.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Hook } from "@/decorators/hook";
|
||||
import { Events } from "@/hooks";
|
||||
import { OsuRenderer } from "@/osu/OsuRenderer";
|
||||
import { p, state } from "@/utils";
|
||||
|
||||
export class OsuControlHooks {
|
||||
@Hook(Events.keyPressed)
|
||||
static keyPressed() {
|
||||
if (p.keyCode == 32) OsuRenderer.setPlaying(!OsuRenderer.playing);
|
||||
}
|
||||
|
||||
@Hook(Events.mouseWheel)
|
||||
static mouseWheel(event: WheelEvent) {
|
||||
if (event.ctrlKey) return;
|
||||
const currentState = state.getState();
|
||||
|
||||
if (
|
||||
currentState.achivementsDialog ||
|
||||
currentState.dataAnalysisDialog ||
|
||||
currentState.metadataEditorDialog ||
|
||||
currentState.openDialog ||
|
||||
currentState.saveDialog
|
||||
)
|
||||
return;
|
||||
|
||||
if (event.deltaY > 0) {
|
||||
OsuRenderer.setTime(OsuRenderer.time + 35);
|
||||
} else {
|
||||
OsuRenderer.setTime(OsuRenderer.time - 35);
|
||||
}
|
||||
}
|
||||
|
||||
@Hook(Events.mousePressed)
|
||||
static mousePressed(event: WheelEvent) {
|
||||
const currentState = state.getState();
|
||||
|
||||
if (
|
||||
currentState.achivementsDialog ||
|
||||
currentState.dataAnalysisDialog ||
|
||||
currentState.metadataEditorDialog ||
|
||||
currentState.openDialog ||
|
||||
currentState.saveDialog
|
||||
)
|
||||
return;
|
||||
}
|
||||
}
|
||||
47
nise-replay-viewer/src/hooks/p5Base.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Events } from "@/hooks";
|
||||
import { Renderer } from "@/renderer";
|
||||
import { p } from "@/utils";
|
||||
import {
|
||||
canvasDragging,
|
||||
canvasMultiplier,
|
||||
canvasTranslation,
|
||||
} from "./canvasControls";
|
||||
import { Hook } from "@/decorators/hook";
|
||||
|
||||
export class p5Hooks {
|
||||
@Hook(Events.setup)
|
||||
static setup() {
|
||||
p.createCanvas(p.windowWidth, p.windowHeight);
|
||||
p.imageMode(p.CENTER);
|
||||
p.frameRate(120);
|
||||
Renderer.setup();
|
||||
Renderer.mouse = p.createVector();
|
||||
}
|
||||
|
||||
@Hook(Events.draw)
|
||||
static draw() {
|
||||
p.cursor("default");
|
||||
if (canvasDragging) {
|
||||
p.cursor("grabbing");
|
||||
}
|
||||
|
||||
p.background(0);
|
||||
p.scale(canvasMultiplier);
|
||||
p.translate(canvasTranslation.x, canvasTranslation.y);
|
||||
|
||||
const translated = p.createVector(p.mouseX, p.mouseY);
|
||||
translated.mult(1 / canvasMultiplier);
|
||||
translated.sub(canvasTranslation);
|
||||
Renderer.mouse.set(translated);
|
||||
|
||||
Renderer.draw();
|
||||
|
||||
p.translate(-canvasTranslation.x, -canvasTranslation.y);
|
||||
p.scale(1 / canvasMultiplier);
|
||||
}
|
||||
|
||||
@Hook(Events.resize)
|
||||
static resize() {
|
||||
p.resizeCanvas(p.windowWidth, p.windowHeight);
|
||||
}
|
||||
}
|
||||
4
nise-replay-viewer/src/hooks/starter.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import "./canvasControls";
|
||||
import "./osuControls";
|
||||
import "./p5Base";
|
||||
import "./tooling";
|
||||
29
nise-replay-viewer/src/hooks/tooling.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Hook } from "@/decorators/hook";
|
||||
import { Events } from "@/hooks";
|
||||
import { Tool } from "@/tooling/tool";
|
||||
|
||||
export class Tooling {
|
||||
static currentTool: Tool | undefined;
|
||||
|
||||
@Hook(Events.mousePressed)
|
||||
static mousePressed() {
|
||||
if (!this.currentTool) return;
|
||||
this.currentTool.mousePressed();
|
||||
}
|
||||
|
||||
@Hook(Events.mousePressed)
|
||||
static mouseReleased() {
|
||||
if (!this.currentTool) return;
|
||||
this.currentTool.mouseReleased();
|
||||
}
|
||||
|
||||
@Hook(Events.draw)
|
||||
static tick() {
|
||||
if (!this.currentTool) return;
|
||||
this.currentTool.tick();
|
||||
}
|
||||
|
||||
static purge() {
|
||||
this.currentTool = undefined;
|
||||
}
|
||||
}
|
||||
38
nise-replay-viewer/src/interface/App.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import {AboutDialog} from "./composites/about-dialog";
|
||||
import {AnalysisSheet} from "./composites/analysis.-sheet";
|
||||
import {Navbar} from "./composites/Menu";
|
||||
import {SongSlider} from "./composites/song-slider";
|
||||
import {Helper} from "./composites/helper";
|
||||
import {useEffect, useState} from "react";
|
||||
import {OsuRenderer} from "@/osu/OsuRenderer";
|
||||
|
||||
export function App() {
|
||||
|
||||
const [replayId, setReplayId] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
let pathReplayId = location.pathname.slice(1, location.pathname.length);
|
||||
|
||||
const loadReplay = async () => {
|
||||
// TODO: Check for security?
|
||||
await OsuRenderer.loadReplayFromUrl(`http://localhost:8080/score/${pathReplayId}/replay`);
|
||||
OsuRenderer.setPlaying(true);
|
||||
// await OsuRenderer.loadReplayFromUrl(`https://nise.moe/api/score/${pathReplayId}/replay`);
|
||||
};
|
||||
|
||||
if(replayId !== pathReplayId) {
|
||||
setReplayId(pathReplayId);
|
||||
loadReplay();
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar/>
|
||||
<AboutDialog/>
|
||||
<AnalysisSheet/>
|
||||
<SongSlider/>
|
||||
<Helper/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vite-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
if (context === undefined)
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
|
||||
return context;
|
||||
};
|
||||
36
nise-replay-viewer/src/interface/components/ui/badge.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
56
nise-replay-viewer/src/interface/components/ui/button.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
79
nise-replay-viewer/src/interface/components/ui/card.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
198
nise-replay-viewer/src/interface/components/ui/context-menu.tsx
Normal file
@ -0,0 +1,198 @@
|
||||
import * as React from "react"
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ContextMenu = ContextMenuPrimitive.Root
|
||||
|
||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
||||
|
||||
const ContextMenuGroup = ContextMenuPrimitive.Group
|
||||
|
||||
const ContextMenuPortal = ContextMenuPrimitive.Portal
|
||||
|
||||
const ContextMenuSub = ContextMenuPrimitive.Sub
|
||||
|
||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
||||
|
||||
const ContextMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
))
|
||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const ContextMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
||||
|
||||
const ContextMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
))
|
||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
||||
|
||||
const ContextMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
||||
|
||||
const ContextMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
ContextMenuCheckboxItem.displayName =
|
||||
ContextMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const ContextMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
))
|
||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const ContextMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
||||
|
||||
const ContextMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
||||
|
||||
const ContextMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
}
|
||||
120
nise-replay-viewer/src/interface/components/ui/dialog.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
25
nise-replay-viewer/src/interface/components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
24
nise-replay-viewer/src/interface/components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
234
nise-replay-viewer/src/interface/components/ui/menubar.tsx
Normal file
@ -0,0 +1,234 @@
|
||||
import * as React from "react"
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const MenubarMenu = MenubarPrimitive.Menu
|
||||
|
||||
const MenubarGroup = MenubarPrimitive.Group
|
||||
|
||||
const MenubarPortal = MenubarPrimitive.Portal
|
||||
|
||||
const MenubarSub = MenubarPrimitive.Sub
|
||||
|
||||
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
|
||||
|
||||
const Menubar = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Menubar.displayName = MenubarPrimitive.Root.displayName
|
||||
|
||||
const MenubarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
|
||||
|
||||
const MenubarSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
))
|
||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
|
||||
|
||||
const MenubarSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
|
||||
|
||||
const MenubarContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
||||
>(
|
||||
(
|
||||
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
|
||||
ref
|
||||
) => (
|
||||
<MenubarPrimitive.Portal>
|
||||
<MenubarPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenubarPrimitive.Portal>
|
||||
)
|
||||
)
|
||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName
|
||||
|
||||
const MenubarItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName
|
||||
|
||||
const MenubarCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
))
|
||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
|
||||
|
||||
const MenubarRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
))
|
||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
|
||||
|
||||
const MenubarLabel = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
|
||||
|
||||
const MenubarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
|
||||
|
||||
const MenubarShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
MenubarShortcut.displayname = "MenubarShortcut"
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarPortal,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarGroup,
|
||||
MenubarSub,
|
||||
MenubarShortcut,
|
||||
}
|
||||
138
nise-replay-viewer/src/interface/components/ui/sheet.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
26
nise-replay-viewer/src/interface/components/ui/slider.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
29
nise-replay-viewer/src/interface/components/ui/sonner.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
82
nise-replay-viewer/src/interface/composites/Menu.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import {
|
||||
Menubar,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
} from "@/interface/components/ui/menubar";
|
||||
import { OsuRenderer } from "@/osu/OsuRenderer";
|
||||
import { state } from "@/utils";
|
||||
|
||||
export function Navbar() {
|
||||
const { beatmap, mods } = state();
|
||||
return (
|
||||
<Menubar className="rounded-none border-x-0 border-t-0 flex justify-between px-4">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={"https://nise.moe/assets/keisatsu-chan.png"} width={48}/>
|
||||
<h3 className="scroll-m-20 text-lg font-semibold tracking-tight">
|
||||
Replay Viewer
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>File</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarItem
|
||||
onClick={() => {
|
||||
state.setState({ aboutDialog: true });
|
||||
}}
|
||||
>
|
||||
About
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
|
||||
{OsuRenderer.beatmap && (
|
||||
<>
|
||||
{" "}
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>Analyzer</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarItem
|
||||
onClick={() => {
|
||||
state.setState({ dataAnalysisDialog: true });
|
||||
}}
|
||||
>
|
||||
gRDA
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{beatmap && (
|
||||
<div className="flex gap-4 items-center">
|
||||
{mods?.map((mod) => {
|
||||
return (
|
||||
<img
|
||||
src={`./mod_${mod.acronym.toLowerCase()}.png`}
|
||||
className="h-5"
|
||||
></img>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-col items-end">
|
||||
<p className="text-xs opacity-75">Currently Viewing</p>
|
||||
|
||||
<p className="text-xs font-bold">
|
||||
{beatmap?.metadata.artist} - {beatmap?.metadata.title}
|
||||
</p>
|
||||
</div>
|
||||
<img
|
||||
src={`https://assets.ppy.sh/beatmaps/${beatmap?.metadata.beatmapSetId}/covers/cover.jpg?${beatmap?.metadata.beatmapId}`}
|
||||
alt=""
|
||||
width={90}
|
||||
className="rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Menubar>
|
||||
);
|
||||
}
|
||||
27
nise-replay-viewer/src/interface/composites/about-dialog.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Button } from "@/interface/components/ui/button";
|
||||
import { Dialog, DialogContent } from "@/interface/components/ui/dialog";
|
||||
import { Badge } from "@/interface/components/ui/badge";
|
||||
import { state } from "@/utils";
|
||||
export function AboutDialog() {
|
||||
const { aboutDialog } = state();
|
||||
return (
|
||||
<Dialog
|
||||
open={aboutDialog}
|
||||
onOpenChange={(change: any) => {
|
||||
state.setState({ aboutDialog: change });
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<div className="bottom-0 left-0 ">
|
||||
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight flex items-center gap-3">
|
||||
Replay Viewer
|
||||
</h3>
|
||||
<div className="opacity-75">
|
||||
<h3 className="text-sm">Originally developed by Assist Games</h3>
|
||||
<h3 className="text-sm">With modifications to be integrated with <strong>nise.moe</strong></h3>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
121
nise-replay-viewer/src/interface/composites/analysis.-sheet.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/interface/components/ui/sheet";
|
||||
import { Button } from "@/interface/components/ui/button";
|
||||
import { BarChart, XAxis, Bar, ResponsiveContainer } from "recharts";
|
||||
import { state } from "@/utils";
|
||||
import { gRDA } from "@/osu/Analysis";
|
||||
import { OsuRenderer } from "@/osu/OsuRenderer";
|
||||
|
||||
export function AnalysisSheet() {
|
||||
const { dataAnalysisDialog, grda } = state();
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
open={dataAnalysisDialog}
|
||||
onOpenChange={(opened: boolean) => {
|
||||
state.setState({ dataAnalysisDialog: opened });
|
||||
}}
|
||||
>
|
||||
<SheetContent
|
||||
side="left"
|
||||
className="min-w-[600px] overflow-scroll overflow-x-hidden SCROLL"
|
||||
>
|
||||
<SheetHeader>
|
||||
<SheetTitle>gRDA</SheetTitle>
|
||||
<SheetDescription>
|
||||
Most of the information provided here is forensic and can be used to
|
||||
detect specific types of cheats, such as Timewarp and Relax. This
|
||||
process might take up to a minute to do and collect all information.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-4"
|
||||
onClick={() => {
|
||||
state.setState({
|
||||
grda: gRDA(OsuRenderer.replay, OsuRenderer.beatmap),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Analyze Replay
|
||||
</Button>
|
||||
{grda && (
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold tracking-tight flex items-center gap-3 mt-6">
|
||||
<span>Response</span>
|
||||
</h3>
|
||||
<div className="opacity-75">
|
||||
<h3 className="text-sm">Frametime Averages:</h3>
|
||||
<ResponsiveContainer height={200} width="100%">
|
||||
<BarChart height={200} data={Object.entries(grda.frameTimes)}>
|
||||
<XAxis dataKey="0" />
|
||||
<Bar dataKey="1" fill="#8884d8" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
<div>Normalization rate due to mods {grda.normalizationRate}</div>
|
||||
<ResponsiveContainer height={200} width="100%">
|
||||
<BarChart
|
||||
height={200}
|
||||
data={Object.entries(grda.moddedFrameTimes)}
|
||||
>
|
||||
<XAxis dataKey="0" />
|
||||
<Bar dataKey="1" fill="#8884d8" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="opacity-75">
|
||||
<h3 className="text-sm">Averages:</h3>
|
||||
<div>
|
||||
Slider Delta Hold Average{" "}
|
||||
{Math.round(
|
||||
(grda.sliderDeltaHoldAverage / grda.sliderLength) * 100
|
||||
) / 100}
|
||||
</div>
|
||||
<div>
|
||||
Approximated circle hold delta range ={" "}
|
||||
{grda.circleExtremes.max - grda.circleExtremes.min}
|
||||
</div>
|
||||
<div>
|
||||
Approximated slider hold delta range ={" "}
|
||||
{grda.sliderExtremes.max - grda.sliderExtremes.min}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>Circle | Holdtime distribution</div>
|
||||
<ResponsiveContainer height={400} width="100%">
|
||||
<BarChart
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
height={400}
|
||||
data={Object.entries(grda.holdCircleDistributionGraph)}
|
||||
>
|
||||
<XAxis dataKey="0" />
|
||||
<Bar dataKey="1" fill="#8884d8" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
<div>Circle | Press time distribution</div>
|
||||
<ResponsiveContainer height={400} width="100%">
|
||||
<BarChart
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
height={400}
|
||||
data={Object.entries(grda.pressCircleDistributionGraph).sort(
|
||||
(a, b) => Number(a[0]) - Number(b[0])
|
||||
)}
|
||||
>
|
||||
<XAxis dataKey="0" />
|
||||
<Bar dataKey="1" fill="#8884d8" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
35
nise-replay-viewer/src/interface/composites/helper.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import middlemouse from "/mouse.svg";
|
||||
import scroll from "/scroll.svg";
|
||||
import space from "/space.svg";
|
||||
import ctrl from "/ctrl.svg";
|
||||
import { state } from "@/utils";
|
||||
|
||||
export function Helper() {
|
||||
const { replay } = state();
|
||||
|
||||
if (!replay) return <></>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col absolute right-10 top-16 gap-2 items-end">
|
||||
<div className="flex gap-2 text-xs items-center">
|
||||
Drag Playfield
|
||||
<img src={middlemouse} />
|
||||
</div>
|
||||
<div className="flex gap-2 text-xs items-center">
|
||||
Seek Beatmap
|
||||
<img src={scroll} />
|
||||
</div>
|
||||
<div className="flex gap-2 text-xs items-center">
|
||||
Pause/Play
|
||||
<img src={space} />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 text-xs items-center">
|
||||
Zoom
|
||||
<img src={ctrl} />
|
||||
<img src={scroll} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
60
nise-replay-viewer/src/interface/composites/song-slider.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { OsuRenderer } from "@/osu/OsuRenderer";
|
||||
import { Card } from "../components/ui/card";
|
||||
import { Slider } from "../components/ui/slider";
|
||||
import { state } from "@/utils";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { ArrowLeft, ArrowRight, PauseIcon, PlayIcon } from "lucide-react";
|
||||
|
||||
export function SongSlider() {
|
||||
const { beatmap, replay, playing, time } = state();
|
||||
if (!beatmap || !replay) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Card className="absolute w-[95%] bottom-10 left-1/2 translate-x-[-50%] p-4 flex flex-col items-center gap-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex flex-col items-start w-full ">
|
||||
<p className="text-sm opacity-50">Current time</p>
|
||||
<p>{new Date(time).toISOString().slice(11, 19)}</p>
|
||||
|
||||
</div>
|
||||
<div className="flex gap-2 w-full justify-center">
|
||||
<Button variant="outline" size="icon"
|
||||
onClick={() => {
|
||||
OsuRenderer.setTime(OsuRenderer.time - 1000);
|
||||
}}>
|
||||
<ArrowLeft />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
OsuRenderer.setPlaying(!playing);
|
||||
}}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
{playing ? <PauseIcon /> : <PlayIcon />}
|
||||
</Button>
|
||||
<Button variant="outline" size="icon"
|
||||
onClick={() => {
|
||||
OsuRenderer.setTime(OsuRenderer.time + 1000);
|
||||
}}>
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex w-full justify-end"></div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<Slider
|
||||
step={10}
|
||||
min={0}
|
||||
max={replay.replay?.length}
|
||||
value={[time]}
|
||||
onValueChange={(value: any) => {
|
||||
OsuRenderer.setTime(value[0]);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
6
nise-replay-viewer/src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
46
nise-replay-viewer/src/main.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import p5 from "p5";
|
||||
import "./style.css";
|
||||
import "./globals.css";
|
||||
import { p, setEnv } from "./utils";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { ThemeProvider } from "./interface/components/theme-provider";
|
||||
import { App } from "./interface/App";
|
||||
import { Events, hookable } from "./hooks";
|
||||
import "@/hooks/starter";
|
||||
import { Toaster } from "./interface/components/ui/sonner";
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
document.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||
const handleWheel = hookable(Events.mouseWheel);
|
||||
window.addEventListener("wheel", handleWheel, { passive: false });
|
||||
|
||||
async function main() {
|
||||
new p5(async (p5Instance) => {
|
||||
setEnv(p5Instance as unknown as p5);
|
||||
|
||||
// Functions that should be expanded into multiple events
|
||||
p.preload = hookable(Events.preload);
|
||||
p.setup = hookable(Events.setup);
|
||||
p.windowResized = hookable(Events.resize);
|
||||
p.draw = hookable(Events.draw);
|
||||
p.mousePressed = hookable(Events.mousePressed);
|
||||
p.mouseDragged = hookable(Events.mouseDragged);
|
||||
p.mouseReleased = hookable(Events.mouseReleased);
|
||||
p.keyPressed = hookable(Events.keyPressed);
|
||||
p.keyReleased = hookable(Events.keyReleased);
|
||||
}, document.getElementById("app")!);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<Router>
|
||||
<App />
|
||||
<Toaster />
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
278
nise-replay-viewer/src/osu/Analysis.ts
Normal file
@ -0,0 +1,278 @@
|
||||
import { modsFromBitmask } from "@osujs/core";
|
||||
import { LegacyReplayFrame, Score } from "osu-classes";
|
||||
import {
|
||||
Circle,
|
||||
StandardBeatmap,
|
||||
StandardHitObject,
|
||||
} from "osu-standard-stable";
|
||||
|
||||
export function getHoldAverages(
|
||||
formattedPresses: {
|
||||
start: number;
|
||||
end: number | null;
|
||||
duration: number;
|
||||
object: null;
|
||||
}[],
|
||||
map: StandardBeatmap
|
||||
) {
|
||||
const circlePresses: {
|
||||
start: number;
|
||||
pressStart: number;
|
||||
holdTime: number;
|
||||
}[] = [];
|
||||
const sliderPresses: {
|
||||
start: number;
|
||||
pressStart: number;
|
||||
holdTime: number;
|
||||
}[] = [];
|
||||
|
||||
let circleHitAverage = 0;
|
||||
let circleLength = 0;
|
||||
let circleExtremes = { max: 0, min: 0 };
|
||||
|
||||
let sliderDeltaHoldAverage = 0;
|
||||
let sliderLength = 0;
|
||||
let sliderExtremes = { max: 0, min: 0 };
|
||||
|
||||
for (const press of formattedPresses) {
|
||||
let closestObject: StandardHitObject = map.hitObjects[0];
|
||||
for (const object of map.hitObjects) {
|
||||
if (
|
||||
Math.abs(press.start - object.startTime) <
|
||||
Math.abs(press.start - closestObject.startTime)
|
||||
) {
|
||||
closestObject = object;
|
||||
}
|
||||
}
|
||||
press.object = closestObject as any;
|
||||
if (closestObject instanceof Circle) {
|
||||
circleHitAverage += press.duration;
|
||||
circleLength++;
|
||||
|
||||
if (press.duration < 160) {
|
||||
circlePresses.push({
|
||||
start: closestObject.startTime,
|
||||
pressStart: press.start,
|
||||
holdTime: press.duration,
|
||||
});
|
||||
if (press.duration > circleExtremes.max) {
|
||||
circleExtremes.max = press.duration;
|
||||
}
|
||||
if (press.duration < circleExtremes.min) {
|
||||
circleExtremes.min = press.duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((closestObject as any).duration) {
|
||||
if (press.duration && (closestObject as any).duration) {
|
||||
let sval = press.duration - (closestObject as any).duration;
|
||||
sliderDeltaHoldAverage += sval;
|
||||
sliderLength++;
|
||||
|
||||
sliderPresses.push({
|
||||
start: closestObject.startTime,
|
||||
pressStart: press.start,
|
||||
holdTime: sval,
|
||||
});
|
||||
|
||||
if (sval > sliderExtremes.max) {
|
||||
sliderExtremes.max = sval;
|
||||
}
|
||||
if (sval < sliderExtremes.min) {
|
||||
sliderExtremes.min = sval;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
circlePresses,
|
||||
sliderPresses,
|
||||
circleLength,
|
||||
sliderDeltaHoldAverage,
|
||||
circleHitAverage,
|
||||
sliderLength,
|
||||
circleExtremes,
|
||||
sliderExtremes,
|
||||
};
|
||||
}
|
||||
|
||||
export function getPresses(score: Score) {
|
||||
let frameTimes: Record<string, number> = {};
|
||||
let moddedFrameTimes: Record<string, number> = {};
|
||||
const mods = modsFromBitmask(score.info.rawMods as number);
|
||||
let normalizationRate = 1;
|
||||
if (mods.includes("DOUBLE_TIME") || mods.includes("NIGHT_CORE")) {
|
||||
normalizationRate = 1.5;
|
||||
}
|
||||
if (mods.includes("HALF_TIME")) {
|
||||
normalizationRate = 0.75;
|
||||
}
|
||||
|
||||
const presses: { start: number; end: number | null; tempEnd: number }[] = [
|
||||
{ start: 0, end: 0, tempEnd: 0 },
|
||||
];
|
||||
|
||||
const presses2: { start: number; end: number | null; tempEnd: number }[] = [
|
||||
{ start: 0, end: 0, tempEnd: 0 },
|
||||
];
|
||||
|
||||
score.replay?.frames.forEach((_frame) => {
|
||||
let frame = _frame as LegacyReplayFrame;
|
||||
if (!frameTimes[frame.interval]) {
|
||||
frameTimes[frame.interval] = 1;
|
||||
} else {
|
||||
frameTimes[frame.interval] += 1;
|
||||
}
|
||||
|
||||
if (!moddedFrameTimes[Math.round(frame.interval / normalizationRate)]) {
|
||||
moddedFrameTimes[Math.round(frame.interval / normalizationRate)] = 1;
|
||||
} else {
|
||||
moddedFrameTimes[Math.round(frame.interval / normalizationRate)] += 1;
|
||||
}
|
||||
|
||||
const lastpress = presses[presses.length - 1];
|
||||
if (frame.mouseLeft) {
|
||||
if (lastpress.end !== null) {
|
||||
presses.push({
|
||||
start: frame.startTime,
|
||||
end: null,
|
||||
tempEnd: frame.startTime,
|
||||
});
|
||||
} else {
|
||||
lastpress.tempEnd = frame.startTime;
|
||||
}
|
||||
} else {
|
||||
if (lastpress.end === null) {
|
||||
lastpress.end = frame.startTime;
|
||||
}
|
||||
}
|
||||
|
||||
const lastpress2 = presses2[presses2.length - 1];
|
||||
if (frame.mouseRight) {
|
||||
if (lastpress2.end !== null) {
|
||||
presses2.push({
|
||||
start: frame.startTime,
|
||||
end: null,
|
||||
tempEnd: frame.startTime,
|
||||
});
|
||||
} else {
|
||||
lastpress2.tempEnd = frame.startTime;
|
||||
}
|
||||
} else {
|
||||
if (lastpress2.end === null) {
|
||||
lastpress2.end = frame.startTime;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { frameTimes, moddedFrameTimes, presses, presses2, normalizationRate };
|
||||
}
|
||||
|
||||
export function getFormattedPresses(
|
||||
presses: {
|
||||
start: number;
|
||||
end: number | null;
|
||||
tempEnd: number;
|
||||
}[],
|
||||
presses2: {
|
||||
start: number;
|
||||
end: number | null;
|
||||
tempEnd: number;
|
||||
}[],
|
||||
map: StandardBeatmap
|
||||
) {
|
||||
const allPresses = [...presses, ...presses2];
|
||||
|
||||
const formattedPresses = allPresses.map((press) => ({
|
||||
start: press.start,
|
||||
end: press.end,
|
||||
duration: press.end! - press.start,
|
||||
object: null,
|
||||
}));
|
||||
|
||||
let joined: any = [];
|
||||
map.hitObjects.forEach((object: any) => {
|
||||
(joined as any).push({
|
||||
start: object.startTime,
|
||||
end: null,
|
||||
sduration: (object as any).duration,
|
||||
});
|
||||
});
|
||||
|
||||
formattedPresses.forEach((object) => {
|
||||
(joined as any).push({
|
||||
start: object.start,
|
||||
end: object.end,
|
||||
duration: object.duration,
|
||||
});
|
||||
});
|
||||
formattedPresses.sort((a, b) => a.start - b.start);
|
||||
joined.sort((a: any, b: any) => a.start - b.start);
|
||||
return { formattedPresses, joined };
|
||||
}
|
||||
|
||||
export function baseRound(
|
||||
x: number,
|
||||
prec: number = 2,
|
||||
base: number = 0.05
|
||||
): number {
|
||||
return Number((Math.round(x / base) * base).toFixed(prec));
|
||||
}
|
||||
|
||||
export function gRDA(score: Score, map: StandardBeatmap) {
|
||||
const { frameTimes, moddedFrameTimes, normalizationRate, presses, presses2 } =
|
||||
getPresses(score);
|
||||
|
||||
const { formattedPresses } = getFormattedPresses(presses, presses2, map);
|
||||
|
||||
const {
|
||||
circlePresses,
|
||||
sliderPresses,
|
||||
circleLength,
|
||||
sliderDeltaHoldAverage,
|
||||
circleHitAverage,
|
||||
sliderLength,
|
||||
circleExtremes,
|
||||
sliderExtremes,
|
||||
} = getHoldAverages(formattedPresses, map);
|
||||
|
||||
const holdCircleDistributionGraph: Record<number, number> = {};
|
||||
|
||||
for (const press of circlePresses) {
|
||||
const normalizedTime = baseRound(press.holdTime, 1, 3);
|
||||
if (!holdCircleDistributionGraph[normalizedTime]) {
|
||||
holdCircleDistributionGraph[normalizedTime] = 1;
|
||||
} else {
|
||||
holdCircleDistributionGraph[normalizedTime] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const pressCircleDistributionGraph: Record<number, number> = {};
|
||||
|
||||
for (const press of circlePresses) {
|
||||
const normalizedTime = baseRound(press.pressStart - press.start, 1, 3);
|
||||
if (!pressCircleDistributionGraph[normalizedTime]) {
|
||||
pressCircleDistributionGraph[normalizedTime] = 1;
|
||||
} else {
|
||||
pressCircleDistributionGraph[normalizedTime] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
frameTimes,
|
||||
moddedFrameTimes,
|
||||
normalizationRate,
|
||||
holdCircleDistributionGraph,
|
||||
pressCircleDistributionGraph,
|
||||
sliderPresses,
|
||||
circlePresses,
|
||||
sliderLength,
|
||||
circleLength,
|
||||
sliderDeltaHoldAverage,
|
||||
circleHitAverage,
|
||||
circleExtremes,
|
||||
sliderExtremes,
|
||||
};
|
||||
}
|
||||
299
nise-replay-viewer/src/osu/Drawer.ts
Normal file
@ -0,0 +1,299 @@
|
||||
import { Vector2 } from "osu-classes";
|
||||
import { Md5 } from "ts-md5";
|
||||
import p5 from "p5";
|
||||
import { loadImageAsync } from "@/utils";
|
||||
|
||||
export class Drawer {
|
||||
private static imageCache: Record<string, p5.Graphics> = {};
|
||||
static images = {
|
||||
cursor: undefined as any as p5.Image,
|
||||
cursortrail: undefined as any as p5.Image,
|
||||
hitcircle: undefined as any as p5.Image,
|
||||
hitcircleoverlay: undefined as any as p5.Image,
|
||||
radix: undefined as any as p5.Image,
|
||||
sliderfollowcircle: undefined as any as p5.Image,
|
||||
sliderb0: undefined as any as p5.Image,
|
||||
default0: undefined as any as p5.Image,
|
||||
default1: undefined as any as p5.Image,
|
||||
default2: undefined as any as p5.Image,
|
||||
default3: undefined as any as p5.Image,
|
||||
default4: undefined as any as p5.Image,
|
||||
default5: undefined as any as p5.Image,
|
||||
default6: undefined as any as p5.Image,
|
||||
default7: undefined as any as p5.Image,
|
||||
default8: undefined as any as p5.Image,
|
||||
default9: undefined as any as p5.Image,
|
||||
};
|
||||
private static p: p5;
|
||||
|
||||
static setP(_p: p5) {
|
||||
this.p = _p;
|
||||
}
|
||||
|
||||
static async loadDefaultImages() {
|
||||
for (const imageName of Object.keys(Drawer.images)) {
|
||||
Drawer.images[imageName as keyof typeof Drawer.images] =
|
||||
await loadImageAsync(`/${imageName}.png`);
|
||||
}
|
||||
}
|
||||
|
||||
static setImages(images: typeof this.images) {
|
||||
this.images = images;
|
||||
}
|
||||
|
||||
static setDrawingOpacity(opacity: number) {
|
||||
//@ts-ignore
|
||||
this.p.drawingContext.globalAlpha = opacity;
|
||||
}
|
||||
|
||||
static drawCircleJudgement(
|
||||
position: Vector2,
|
||||
radius: number,
|
||||
judgement: string
|
||||
) {
|
||||
Drawer.p.push();
|
||||
Drawer.p.strokeWeight(2);
|
||||
if (judgement === "OK") {
|
||||
Drawer.p.stroke(`rgb(106, 176, 76)`);
|
||||
Drawer.p.fill(`rgb(106, 176, 76)`);
|
||||
}
|
||||
if (judgement === "MEH") {
|
||||
Drawer.p.stroke(`rgb(241, 196, 15)`);
|
||||
Drawer.p.fill(`rgb(241, 196, 15)`);
|
||||
}
|
||||
if (judgement === "MISS") {
|
||||
Drawer.p.stroke(`rgb(231, 76, 60)`);
|
||||
Drawer.p.fill(`rgb(231, 76, 60)`);
|
||||
}
|
||||
if (judgement !== "GREAT") {
|
||||
Drawer.p.circle(position.x, position.y, radius * 2);
|
||||
}
|
||||
Drawer.p.pop();
|
||||
}
|
||||
|
||||
static drawSliderFollowPoint(position: Vector2, radius: number) {
|
||||
Drawer.p.image(
|
||||
this.images.sliderfollowcircle,
|
||||
position.x,
|
||||
position.y,
|
||||
radius * 2,
|
||||
radius * 2
|
||||
);
|
||||
Drawer.p.push();
|
||||
Drawer.p.fill(255, 255, 255, 18);
|
||||
Drawer.p.circle(position.x, position.y, radius * 4);
|
||||
Drawer.p.pop();
|
||||
}
|
||||
|
||||
static drawHitCircle(position: Vector2, radius: number, comboNumber: number) {
|
||||
Drawer.p.push();
|
||||
Drawer.p.noStroke();
|
||||
Drawer.p.fill(160);
|
||||
Drawer.p.image(
|
||||
this.images.hitcircle,
|
||||
position.x,
|
||||
position.y,
|
||||
radius * 2,
|
||||
radius * 2
|
||||
);
|
||||
Drawer.p.image(
|
||||
this.images.hitcircleoverlay,
|
||||
position.x,
|
||||
position.y,
|
||||
radius * 2,
|
||||
radius * 2
|
||||
);
|
||||
this.drawNumberWithSprites(
|
||||
comboNumber + 1,
|
||||
new Vector2(position.x - 0.5, position.y),
|
||||
radius * 0.4
|
||||
);
|
||||
|
||||
Drawer.p.pop();
|
||||
}
|
||||
|
||||
static drawApproachCircle(
|
||||
position: Vector2,
|
||||
radius: number,
|
||||
arScale: number
|
||||
) {
|
||||
if (arScale == 1) return;
|
||||
Drawer.p.push();
|
||||
Drawer.p.noFill();
|
||||
Drawer.p.stroke("white");
|
||||
Drawer.p.strokeWeight(3);
|
||||
Drawer.p.circle(position.x, position.y, radius * 2 * arScale);
|
||||
Drawer.p.pop();
|
||||
}
|
||||
|
||||
static drawSliderBody(origin: Vector2, path: Vector2[], radius: number) {
|
||||
Drawer.p.push();
|
||||
|
||||
const cacheKey = Md5.hashStr(JSON.stringify(path) + JSON.stringify(radius));
|
||||
if (!this.imageCache[cacheKey]) {
|
||||
const g = Drawer.p.createGraphics(512 * 4, 384 * 4);
|
||||
g.scale(2);
|
||||
g.translate(512 - 256, 384 - 192);
|
||||
//@ts-ignore
|
||||
const ctx = g.drawingContext;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
g.translate(origin.x, origin.y);
|
||||
g.noFill();
|
||||
|
||||
g.strokeWeight(radius * 2 - 10);
|
||||
|
||||
g.stroke(255);
|
||||
g.beginShape();
|
||||
for (const node of path) {
|
||||
g.vertex(node.x, node.y);
|
||||
}
|
||||
g.endShape();
|
||||
|
||||
g.strokeWeight(radius * 2 - 17);
|
||||
g.stroke(10);
|
||||
g.beginShape();
|
||||
for (const node of path) {
|
||||
g.vertex(node.x, node.y);
|
||||
}
|
||||
|
||||
g.endShape();
|
||||
|
||||
for (let i = 0; i < radius * 2 - 17; i += 2) {
|
||||
g.strokeWeight(radius * 2 - 17 - i);
|
||||
g.stroke(Math.round((i / (radius * 2 - 17)) * 45));
|
||||
g.beginShape();
|
||||
for (const node of path) {
|
||||
g.vertex(node.x, node.y);
|
||||
}
|
||||
|
||||
g.endShape();
|
||||
}
|
||||
|
||||
this.imageCache[cacheKey] = g;
|
||||
}
|
||||
Drawer.p.imageMode(Drawer.p.CORNER);
|
||||
Drawer.p.image(this.imageCache[cacheKey], -256, -192, 512 * 2, 384 * 2);
|
||||
|
||||
Drawer.p.pop();
|
||||
}
|
||||
|
||||
static drawCursorPath(
|
||||
path: {
|
||||
position: Vector2;
|
||||
button: {
|
||||
mouseLeft1: boolean;
|
||||
mouseLeft2: boolean;
|
||||
mouseRight1: boolean;
|
||||
mouseRight2: boolean;
|
||||
};
|
||||
}[],
|
||||
cursor: {
|
||||
position: Vector2;
|
||||
button: {
|
||||
mouseLeft1: boolean;
|
||||
mouseLeft2: boolean;
|
||||
mouseRight1: boolean;
|
||||
mouseRight2: boolean;
|
||||
};
|
||||
}
|
||||
) {
|
||||
Drawer.p.push();
|
||||
//@ts-ignore
|
||||
const ctx = Drawer.p.drawingContext;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
Drawer.p.noFill();
|
||||
|
||||
Drawer.p.push();
|
||||
Drawer.p.strokeWeight(2.5);
|
||||
Drawer.p.stroke("black");
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const lastFrame = path[i - 1];
|
||||
const frame = path[i];
|
||||
|
||||
Drawer.p.line(
|
||||
lastFrame.position.x,
|
||||
lastFrame.position.y,
|
||||
frame.position.x,
|
||||
frame.position.y
|
||||
);
|
||||
}
|
||||
|
||||
Drawer.p.pop();
|
||||
Drawer.p.strokeWeight(1.5);
|
||||
Drawer.p.stroke("white");
|
||||
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const lastFrame = path[i - 1];
|
||||
const frame = path[i];
|
||||
Drawer.p.stroke("white");
|
||||
|
||||
if (lastFrame.button.mouseLeft1 || lastFrame.button.mouseLeft2) {
|
||||
Drawer.p.stroke("#BB6BD9");
|
||||
}
|
||||
if (lastFrame.button.mouseRight1 || lastFrame.button.mouseRight2) {
|
||||
Drawer.p.stroke("#F2994A");
|
||||
}
|
||||
if (lastFrame.button.mouseLeft1 && lastFrame.button.mouseRight1) {
|
||||
Drawer.p.stroke("red");
|
||||
}
|
||||
Drawer.p.line(
|
||||
lastFrame.position.x,
|
||||
lastFrame.position.y,
|
||||
frame.position.x,
|
||||
frame.position.y
|
||||
);
|
||||
}
|
||||
|
||||
if (cursor.position)
|
||||
Drawer.p.image(
|
||||
this.images.cursor,
|
||||
cursor.position.x,
|
||||
cursor.position.y,
|
||||
55,
|
||||
55
|
||||
);
|
||||
|
||||
Drawer.p.pop();
|
||||
}
|
||||
static drawNumberWithSprites(
|
||||
number: number,
|
||||
position: Vector2,
|
||||
size: number
|
||||
) {
|
||||
Drawer.p.push();
|
||||
Drawer.p.imageMode(Drawer.p.CORNER);
|
||||
const digits = number.toString().split("");
|
||||
const digitWidth = size;
|
||||
const digitHeight = size * 1.2;
|
||||
const digitSpacing = -size * 0.1;
|
||||
const totalWidth = digits.length * (digitWidth + digitSpacing);
|
||||
const x = position.x - totalWidth / 2;
|
||||
const y = position.y - digitHeight / 2;
|
||||
digits.forEach((digit, index) => {
|
||||
const indexer = `default${digit}`;
|
||||
const image = this.images[indexer as keyof typeof this.images];
|
||||
Drawer.p.image(
|
||||
image,
|
||||
x + index * (digitWidth + digitSpacing),
|
||||
y,
|
||||
digitWidth,
|
||||
digitHeight
|
||||
);
|
||||
});
|
||||
Drawer.p.pop();
|
||||
}
|
||||
static drawField() {
|
||||
Drawer.p.noFill();
|
||||
Drawer.p.stroke(255, 255, 255, 60);
|
||||
Drawer.p.rect(0, 0, 512, 384, 4);
|
||||
}
|
||||
|
||||
static beginDrawing() {
|
||||
Drawer.p.push();
|
||||
}
|
||||
static endDrawing() {
|
||||
Drawer.p.pop();
|
||||
}
|
||||
}
|
||||
413
nise-replay-viewer/src/osu/OsuRenderer.ts
Normal file
@ -0,0 +1,413 @@
|
||||
import {HitResult, LegacyReplayFrame, Score, Vector2} from "osu-classes";
|
||||
import { BeatmapDecoder, BeatmapEncoder } from "osu-parsers";
|
||||
import {
|
||||
Circle,
|
||||
Slider,
|
||||
Spinner,
|
||||
StandardBeatmap,
|
||||
StandardHardRock,
|
||||
StandardHitObject,
|
||||
StandardModCombination,
|
||||
StandardRuleset,
|
||||
} from "osu-standard-stable";
|
||||
import { Drawer } from "./Drawer";
|
||||
import { Vec2 } from "@osujs/math";
|
||||
import { clamp, getBeatmap, getReplay } from "@/utils";
|
||||
import EventEmitter from "eventemitter3";
|
||||
|
||||
export enum OsuRendererEvents {
|
||||
UPDATE = "UPDATE",
|
||||
LOAD = "LOAD",
|
||||
PLAY = "PLAY",
|
||||
TIME = "TIME",
|
||||
}
|
||||
|
||||
export class OsuRendererBridge extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class OsuRenderer {
|
||||
private static preempt: number;
|
||||
private static fadeIn: number;
|
||||
private static lastRender: number = Date.now();
|
||||
|
||||
static playing: boolean = false;
|
||||
|
||||
static event = new OsuRendererBridge();
|
||||
|
||||
static time: number = 0;
|
||||
static beatmap: StandardBeatmap;
|
||||
static og_beatmap: StandardBeatmap;
|
||||
static replay: Score;
|
||||
static og_replay_mods: StandardModCombination;
|
||||
static forceHR: boolean | undefined = undefined;
|
||||
|
||||
static loaded: boolean = false;
|
||||
|
||||
static pathWindow: number = 500;
|
||||
|
||||
static purge() {
|
||||
this.replay = undefined as any;
|
||||
this.beatmap = undefined as any;
|
||||
}
|
||||
|
||||
static render() {
|
||||
if (!this.beatmap || !this.replay) return;
|
||||
|
||||
if (this.time >= this.replay.replay!.length - 500)
|
||||
this.time = this.replay.replay!.length - 500;
|
||||
|
||||
for (let i = this.beatmap.hitObjects.length - 1; i >= 0; i--) {
|
||||
this.renderObject(this.beatmap.hitObjects[i]);
|
||||
}
|
||||
|
||||
if (this.playing) {
|
||||
this.setTime(this.time + (Date.now() - this.lastRender));
|
||||
}
|
||||
|
||||
this.lastRender = Date.now();
|
||||
|
||||
this.renderPath();
|
||||
Drawer.drawField();
|
||||
}
|
||||
|
||||
static getOptions() {
|
||||
return {
|
||||
replay: this.replay,
|
||||
mods: this.replay.info.mods?.all,
|
||||
beatmap: this.og_beatmap,
|
||||
};
|
||||
}
|
||||
|
||||
static getCurrentDifficulty() {
|
||||
return {
|
||||
AR: OsuRenderer.og_beatmap?.difficulty?.approachRate,
|
||||
CS: OsuRenderer.og_beatmap?.difficulty?.circleSize,
|
||||
OD: OsuRenderer.og_beatmap?.difficulty?.overallDifficulty,
|
||||
};
|
||||
}
|
||||
|
||||
static setMetadata({ AR, CS, OD }: { AR: number; CS: number; OD: number }) {
|
||||
this.og_beatmap.difficulty.approachRate = AR;
|
||||
this.og_beatmap.difficulty.circleSize = CS;
|
||||
this.og_beatmap.difficulty.overallDifficulty = OD;
|
||||
const tempClone = this.og_beatmap.clone();
|
||||
let sendMods = this.replay.info.mods as StandardModCombination;
|
||||
const hasHardrock = sendMods.all.some((e) => e instanceof StandardHardRock);
|
||||
|
||||
if (this.forceHR !== undefined) {
|
||||
if (hasHardrock !== this.forceHR) {
|
||||
this.replay.replay?.frames.forEach((frame) => {
|
||||
const f = frame as LegacyReplayFrame;
|
||||
f.position = new Vector2(f.position.x, 384 - f.position.y);
|
||||
});
|
||||
}
|
||||
if (this.forceHR) {
|
||||
if (!hasHardrock) {
|
||||
//@ts-ignore
|
||||
sendMods._mods.push(new StandardHardRock());
|
||||
}
|
||||
} else {
|
||||
//@ts-ignore
|
||||
sendMods._mods = sendMods._mods.filter(
|
||||
//@ts-ignore
|
||||
(e) => !(e instanceof StandardHardRock)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.beatmap = this.recreateBeatmap(tempClone, sendMods);
|
||||
// GameplayAnalyzer.createBucket(this.replay, this.beatmap);
|
||||
|
||||
let fadeIn: number;
|
||||
let preempt: number;
|
||||
if (this.beatmap!.difficulty.approachRate <= 5) {
|
||||
fadeIn = 800 + (400 * (5 - this.beatmap!.difficulty.approachRate)) / 5;
|
||||
preempt = 1200 + (600 * (5 - this.beatmap!.difficulty.approachRate)) / 5;
|
||||
} else {
|
||||
fadeIn = 800 - (500 * (this.beatmap!.difficulty.approachRate - 5)) / 5;
|
||||
preempt = 1200 - (750 * (this.beatmap!.difficulty.approachRate - 5)) / 5;
|
||||
}
|
||||
this.preempt = preempt;
|
||||
this.fadeIn = fadeIn;
|
||||
this.event.emit(OsuRendererEvents.UPDATE);
|
||||
}
|
||||
|
||||
static setPlaying(state: boolean) {
|
||||
this.playing = state;
|
||||
this.event.emit(OsuRendererEvents.PLAY);
|
||||
}
|
||||
|
||||
static refreshMetadata() {
|
||||
OsuRenderer.setMetadata({
|
||||
AR: OsuRenderer.og_beatmap.difficulty.approachRate,
|
||||
CS: OsuRenderer.og_beatmap.difficulty.circleSize,
|
||||
OD: OsuRenderer.og_beatmap.difficulty.overallDifficulty,
|
||||
});
|
||||
}
|
||||
|
||||
static async loadReplayFromUrl(url: string) {
|
||||
OsuRenderer.purge();
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-NISE-REPLAY': '20240303'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const { beatmap, replay, mods } = data;
|
||||
|
||||
const i_replay = await getReplay(replay);
|
||||
i_replay.info.rawMods = mods;
|
||||
i_replay.info.mods = new StandardModCombination(mods);
|
||||
|
||||
const i_beatmap = await getBeatmap(beatmap, i_replay);
|
||||
|
||||
OsuRenderer.setOptions(i_beatmap, i_replay);
|
||||
this.event.emit(OsuRendererEvents.LOAD);
|
||||
}
|
||||
|
||||
static setOptions(beatmap: StandardBeatmap, replay: Score) {
|
||||
this.forceHR = undefined;
|
||||
this.replay = replay;
|
||||
this.beatmap = beatmap;
|
||||
this.og_beatmap = beatmap.clone();
|
||||
this.og_replay_mods = replay.info.mods?.clone() as StandardModCombination;
|
||||
this.setMetadata({
|
||||
AR: this.beatmap.difficulty.approachRate,
|
||||
CS: this.beatmap.difficulty.circleSize,
|
||||
OD: this.beatmap.difficulty.overallDifficulty,
|
||||
});
|
||||
}
|
||||
|
||||
static setTime(time: number) {
|
||||
this.time = clamp(time, 0, OsuRenderer.replay.replay?.length || 0);
|
||||
this.event.emit(OsuRendererEvents.TIME);
|
||||
}
|
||||
|
||||
private static renderObject(hitObject: StandardHitObject) {
|
||||
if (hitObject instanceof Circle) {
|
||||
this.renderCircle(hitObject);
|
||||
}
|
||||
|
||||
if (hitObject instanceof Slider) {
|
||||
this.renderSlider(hitObject);
|
||||
}
|
||||
}
|
||||
|
||||
private static calculateEffects(hitObject: StandardHitObject) {
|
||||
let vEndTime = hitObject.startTime;
|
||||
|
||||
if (hitObject instanceof Slider || hitObject instanceof Spinner) {
|
||||
vEndTime = hitObject.endTime + 25;
|
||||
}
|
||||
|
||||
const fadeOut = Math.max(
|
||||
0.0,
|
||||
(this.time - vEndTime) / hitObject.hitWindows.windowFor(HitResult.Meh)
|
||||
);
|
||||
|
||||
let opacity = Math.max(
|
||||
0.0,
|
||||
Math.min(
|
||||
1.0,
|
||||
Math.min(
|
||||
1.0,
|
||||
(this.time - hitObject.startTime + this.preempt) / this.fadeIn
|
||||
) - fadeOut
|
||||
)
|
||||
);
|
||||
|
||||
const arScale = Math.max(
|
||||
1,
|
||||
((hitObject.startTime - this.time) / this.preempt) * 3.0 + 1.0
|
||||
);
|
||||
|
||||
let visible =
|
||||
this.time > hitObject.startTime - this.preempt &&
|
||||
this.time < vEndTime + hitObject.hitWindows.windowFor(HitResult.Meh);
|
||||
|
||||
if (hitObject instanceof Slider && this.time > hitObject.endTime) {
|
||||
opacity -= (this.time - hitObject.endTime) / 25;
|
||||
}
|
||||
return {
|
||||
opacity,
|
||||
arScale,
|
||||
visible,
|
||||
};
|
||||
}
|
||||
|
||||
private static renderCircle(hitObject: Circle) {
|
||||
if (hitObject.startTime > this.time + 10000) return;
|
||||
const { arScale, opacity, visible } = this.calculateEffects(hitObject);
|
||||
|
||||
if (!visible) return;
|
||||
Drawer.beginDrawing();
|
||||
|
||||
Drawer.setDrawingOpacity(opacity);
|
||||
|
||||
Drawer.drawApproachCircle(
|
||||
hitObject.stackedStartPosition,
|
||||
hitObject.radius,
|
||||
arScale
|
||||
);
|
||||
Drawer.drawHitCircle(
|
||||
hitObject.stackedStartPosition,
|
||||
hitObject.radius,
|
||||
hitObject.currentComboIndex
|
||||
);
|
||||
|
||||
// if (GameplayAnalyzer.renderJudgements[hitObject.startTime]) {
|
||||
// Drawer.setDrawingOpacity(opacity / 2);
|
||||
//
|
||||
// Drawer.drawCircleJudgement(
|
||||
// hitObject.stackedStartPosition,
|
||||
// hitObject.radius,
|
||||
// GameplayAnalyzer.renderJudgements[hitObject.startTime]
|
||||
// );
|
||||
// }
|
||||
Drawer.endDrawing();
|
||||
return arScale;
|
||||
}
|
||||
|
||||
private static renderSlider(hitObject: Slider) {
|
||||
if (hitObject.endTime > this.time + 10000) return;
|
||||
|
||||
const { arScale, opacity, visible } = this.calculateEffects(hitObject);
|
||||
|
||||
if (!visible) return;
|
||||
Drawer.beginDrawing();
|
||||
Drawer.setDrawingOpacity(opacity * 0.8);
|
||||
|
||||
Drawer.drawSliderBody(
|
||||
hitObject.stackedStartPosition,
|
||||
hitObject.path.path,
|
||||
hitObject.radius
|
||||
);
|
||||
Drawer.setDrawingOpacity(opacity);
|
||||
|
||||
Drawer.drawApproachCircle(
|
||||
hitObject.stackedStartPosition,
|
||||
hitObject.radius,
|
||||
arScale
|
||||
);
|
||||
Drawer.drawHitCircle(
|
||||
hitObject.stackedStartPosition,
|
||||
hitObject.radius,
|
||||
hitObject.currentComboIndex
|
||||
);
|
||||
|
||||
let progress = (this.time - hitObject.startTime) / hitObject.duration;
|
||||
let position = hitObject.stackedStartPosition.add(
|
||||
hitObject.path.curvePositionAt(progress, hitObject.repeats + 1)
|
||||
);
|
||||
|
||||
if (hitObject.repeats == 0) {
|
||||
position = hitObject.stackedStartPosition.add(
|
||||
hitObject.path.positionAt(progress)
|
||||
);
|
||||
}
|
||||
|
||||
let sliderPos = new Vector2(position.x, position.y);
|
||||
|
||||
if (this.time > hitObject.startTime && this.time < hitObject.endTime) {
|
||||
Drawer.drawSliderFollowPoint(sliderPos, hitObject.radius);
|
||||
}
|
||||
|
||||
// if (GameplayAnalyzer.renderJudgements[hitObject.startTime]) {
|
||||
// Drawer.setDrawingOpacity(opacity / 2);
|
||||
|
||||
// Drawer.drawCircleJudgement(
|
||||
// hitObject.stackedStartPosition,
|
||||
// hitObject.radius,
|
||||
// GameplayAnalyzer.renderJudgements[hitObject.startTime]
|
||||
// );
|
||||
// }
|
||||
Drawer.endDrawing();
|
||||
return arScale;
|
||||
}
|
||||
|
||||
private static renderPath() {
|
||||
const frames = this.replay.replay!.frames as LegacyReplayFrame[];
|
||||
const renderFrames: {
|
||||
position: Vector2;
|
||||
button: {
|
||||
mouseLeft1: boolean;
|
||||
mouseLeft2: boolean;
|
||||
mouseRight1: boolean;
|
||||
mouseRight2: boolean;
|
||||
};
|
||||
}[] = [];
|
||||
let lastFrame: LegacyReplayFrame | undefined;
|
||||
let cursorPushed: any = false;
|
||||
for (const frame of frames) {
|
||||
if (frame.startTime > this.time) {
|
||||
if (!cursorPushed)
|
||||
cursorPushed = {
|
||||
position: this.interpolateReplayPosition(
|
||||
lastFrame ? lastFrame : frame,
|
||||
frame,
|
||||
this.time
|
||||
),
|
||||
button: {
|
||||
mouseLeft1: frame.mouseLeft1,
|
||||
mouseLeft2: frame.mouseLeft2,
|
||||
mouseRight1: frame.mouseRight1,
|
||||
mouseRight2: frame.mouseRight2,
|
||||
},
|
||||
};
|
||||
if (frame.startTime > this.time + this.pathWindow) break;
|
||||
}
|
||||
if (frame.startTime < this.time - this.pathWindow) {
|
||||
continue;
|
||||
}
|
||||
lastFrame = frame;
|
||||
// renderFrames.push({
|
||||
// position: frame.position,
|
||||
// button: {
|
||||
// mouseLeft1: frame.mouseLeft1,
|
||||
// mouseLeft2: frame.mouseLeft2,
|
||||
// mouseRight1: frame.mouseRight1,
|
||||
// mouseRight2: frame.mouseRight2,
|
||||
// },
|
||||
// });
|
||||
}
|
||||
|
||||
Drawer.drawCursorPath(renderFrames, cursorPushed);
|
||||
|
||||
return cursorPushed;
|
||||
}
|
||||
|
||||
private static interpolateReplayPosition(
|
||||
fA: LegacyReplayFrame,
|
||||
fB: LegacyReplayFrame,
|
||||
time: number
|
||||
): Vector2 {
|
||||
if (fB.startTime === fA.startTime) {
|
||||
return fA.position;
|
||||
} else {
|
||||
const p = (time - fA.startTime) / (fB.startTime - fA.startTime);
|
||||
const { x, y } = Vec2.interpolate(fA.position, fB.position, p);
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
private static recreateBeatmap(
|
||||
beatmap: StandardBeatmap,
|
||||
mods: StandardModCombination
|
||||
) {
|
||||
const beatmapDecoder = new BeatmapDecoder();
|
||||
const beatmapEncoder = new BeatmapEncoder();
|
||||
const ruleset = new StandardRuleset();
|
||||
|
||||
const bp = beatmapDecoder.decodeFromString(
|
||||
beatmapEncoder.encodeToString(beatmap)
|
||||
);
|
||||
return ruleset.applyToBeatmapWithMods(bp, mods as StandardModCombination);
|
||||
}
|
||||
|
||||
}
|
||||
50
nise-replay-viewer/src/renderer.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Vector } from "p5";
|
||||
import { p, state } from "./utils";
|
||||
import { OsuRenderer, OsuRendererEvents } from "./osu/OsuRenderer";
|
||||
import { Drawer } from "./osu/Drawer";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export class Renderer {
|
||||
static mouse: Vector;
|
||||
static OsuRenderer: OsuRenderer = OsuRenderer;
|
||||
|
||||
static async setup() {
|
||||
Renderer.registerEvents();
|
||||
Drawer.setP(p);
|
||||
|
||||
await Drawer.loadDefaultImages();
|
||||
}
|
||||
|
||||
static draw() {
|
||||
if (!OsuRenderer.beatmap) return;
|
||||
|
||||
OsuRenderer.render();
|
||||
p.circle(this.mouse.x, this.mouse.y, 25);
|
||||
}
|
||||
|
||||
static registerEvents() {
|
||||
// Sync UI with datapath classes
|
||||
OsuRenderer.event.on(OsuRendererEvents.UPDATE, () => {
|
||||
const options = OsuRenderer.getOptions();
|
||||
state.setState({
|
||||
beatmap: options.beatmap,
|
||||
replay: options.replay,
|
||||
mods: options.mods,
|
||||
});
|
||||
});
|
||||
|
||||
OsuRenderer.event.on(OsuRendererEvents.LOAD, () => {
|
||||
toast(`Successfully loaded replay!`);
|
||||
});
|
||||
OsuRenderer.event.on(OsuRendererEvents.PLAY, () => {
|
||||
state.setState({
|
||||
playing: OsuRenderer.playing,
|
||||
});
|
||||
});
|
||||
OsuRenderer.event.on(OsuRendererEvents.TIME, () => {
|
||||
state.setState({
|
||||
time: OsuRenderer.time,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
35
nise-replay-viewer/src/style.css
Normal file
@ -0,0 +1,35 @@
|
||||
#app {
|
||||
margin:0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#root{
|
||||
}
|
||||
|
||||
body{
|
||||
margin:0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f10a;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #88888852;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
15
nise-replay-viewer/src/tooling/brush.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Tool } from "./tool";
|
||||
import { Singleton } from "@/decorators/singleton";
|
||||
|
||||
@Singleton
|
||||
export class BrushTool extends Tool {
|
||||
mousePressed(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
mouseReleased(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
tick(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
5
nise-replay-viewer/src/tooling/tool.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export abstract class Tool {
|
||||
abstract mousePressed(): void;
|
||||
abstract mouseReleased(): void;
|
||||
abstract tick(): void;
|
||||
}
|
||||
84
nise-replay-viewer/src/utils.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { BeatmapDecoder } from "osu-parsers";
|
||||
import { ScoreDecoder } from "../osu-parsers";
|
||||
import { StandardRuleset, StandardBeatmap } from "osu-standard-stable";
|
||||
|
||||
import { IMod, Score } from "osu-classes";
|
||||
import p5, { Image } from "p5";
|
||||
import { create } from "zustand";
|
||||
|
||||
const ruleset = new StandardRuleset();
|
||||
const scoreDecoder = new ScoreDecoder();
|
||||
const beatmapDecoder = new BeatmapDecoder();
|
||||
|
||||
export async function getReplay(buffer: ArrayBuffer) {
|
||||
const repl = await scoreDecoder.decodeFromBuffer(buffer);
|
||||
repl.info.ruleset = ruleset;
|
||||
return repl;
|
||||
}
|
||||
|
||||
export async function getBeatmap(mapText: string, scoreBase: Score) {
|
||||
const map = beatmapDecoder.decodeFromString(mapText);
|
||||
|
||||
const score = scoreBase.info;
|
||||
score.accuracy =
|
||||
(score.count300 + score.count100 / 3 + score.count50 / 6) /
|
||||
(score.count300 + score.count100 + score.count50 + score.countMiss);
|
||||
|
||||
return ruleset.applyToBeatmap(map);
|
||||
}
|
||||
|
||||
export async function loadImageAsync(image: string): Promise<Image> {
|
||||
return new Promise((res) => {
|
||||
p.loadImage(image, (img) => {
|
||||
res(img);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function clamp(num: number, min: number, max: number) {
|
||||
if (num > max) return max;
|
||||
if (num < min) return min;
|
||||
return num
|
||||
}
|
||||
|
||||
export const state = create<{
|
||||
beatmap: StandardBeatmap | null;
|
||||
replay: Score | null;
|
||||
metadataEditorDialog: boolean;
|
||||
openDialog: boolean;
|
||||
saveDialog: boolean;
|
||||
dataAnalysisDialog: boolean;
|
||||
achivementsDialog: boolean;
|
||||
aboutDialog: boolean;
|
||||
grda: any;
|
||||
tool: "cursor" | "brush" | "advanced" | "smoother";
|
||||
mods: IMod[] | null;
|
||||
playing: boolean;
|
||||
time: number;
|
||||
}>(() => ({
|
||||
metadataEditorDialog: false,
|
||||
openDialog: false,
|
||||
saveDialog: false,
|
||||
aboutDialog: false,
|
||||
achivementsDialog: false,
|
||||
dataAnalysisDialog: false,
|
||||
beatmap: null,
|
||||
replay: null,
|
||||
grda: null,
|
||||
tool: "cursor",
|
||||
mods: null,
|
||||
playing: false,
|
||||
time: 0,
|
||||
}));
|
||||
|
||||
state.subscribe((newState) => {
|
||||
if (newState.beatmap) {
|
||||
document.title = `${newState.beatmap.metadata.artist} - ${newState.beatmap.metadata.titleUnicode} | Replay Inspector`
|
||||
}
|
||||
})
|
||||
|
||||
export let p: p5;
|
||||
|
||||
export function setEnv(_p: p5) {
|
||||
p = _p;
|
||||
}
|
||||
76
nise-replay-viewer/tailwind.config.js
Normal file
@ -0,0 +1,76 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
||||
25
nise-replay-viewer/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"types": ["vite/client"],
|
||||
"noEmit": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./src", "node_modules/@types/p5/index.d.ts"]
|
||||
}
|
||||
12
nise-replay-viewer/vite.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import path from "path";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||