├── .env.production
├── .eslintrc.cjs
├── .gitignore
├── .stylelintrc.json
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.cjs
├── public
├── assets
│ ├── chunithm
│ │ ├── character
│ │ │ ├── copper.webp
│ │ │ ├── gold.webp
│ │ │ ├── holographic.webp
│ │ │ ├── normal.webp
│ │ │ ├── platina.webp
│ │ │ ├── rainbow.webp
│ │ │ └── silver.webp
│ │ ├── class_emblem
│ │ │ ├── base
│ │ │ │ ├── 1.webp
│ │ │ │ ├── 2.webp
│ │ │ │ ├── 3.webp
│ │ │ │ ├── 4.webp
│ │ │ │ ├── 5.webp
│ │ │ │ └── 6.webp
│ │ │ └── medal
│ │ │ │ ├── 1.webp
│ │ │ │ ├── 2.webp
│ │ │ │ ├── 3.webp
│ │ │ │ ├── 4.webp
│ │ │ │ ├── 5.webp
│ │ │ │ └── 6.webp
│ │ ├── music_icon
│ │ │ ├── absolute.webp
│ │ │ ├── absolutep.webp
│ │ │ ├── absolutepp.webp
│ │ │ ├── alljustice.webp
│ │ │ ├── alljustice_s.webp
│ │ │ ├── alljustice_xs.webp
│ │ │ ├── alljusticecritical.webp
│ │ │ ├── alljusticecritical_s.webp
│ │ │ ├── alljusticecritical_xs.webp
│ │ │ ├── blank_xs.webp
│ │ │ ├── catastrophy.webp
│ │ │ ├── clear.webp
│ │ │ ├── clear_s.webp
│ │ │ ├── failed.webp
│ │ │ ├── fullchain.webp
│ │ │ ├── fullchain2.webp
│ │ │ ├── fullchain2_xs.webp
│ │ │ ├── fullchain_blank.webp
│ │ │ ├── fullchain_xs.webp
│ │ │ ├── fullcombo.webp
│ │ │ ├── fullcombo_blank.webp
│ │ │ ├── fullcombo_s.webp
│ │ │ ├── fullcombo_xs.webp
│ │ │ └── hard.webp
│ │ ├── music_rank
│ │ │ ├── a.webp
│ │ │ ├── a_s.webp
│ │ │ ├── aa.webp
│ │ │ ├── aa_s.webp
│ │ │ ├── aaa.webp
│ │ │ ├── aaa_s.webp
│ │ │ ├── b.webp
│ │ │ ├── b_s.webp
│ │ │ ├── bb.webp
│ │ │ ├── bb_s.webp
│ │ │ ├── bbb.webp
│ │ │ ├── bbb_s.webp
│ │ │ ├── c.webp
│ │ │ ├── c_s.webp
│ │ │ ├── d.webp
│ │ │ ├── d_s.webp
│ │ │ ├── s.webp
│ │ │ ├── s_s.webp
│ │ │ ├── sp.webp
│ │ │ ├── sp_s.webp
│ │ │ ├── ss.webp
│ │ │ ├── ss_s.webp
│ │ │ ├── ssp.webp
│ │ │ ├── ssp_s.png
│ │ │ ├── ssp_s.webp
│ │ │ ├── sss.webp
│ │ │ ├── sss_s.webp
│ │ │ ├── sssp.webp
│ │ │ └── sssp_s.webp
│ │ ├── reborn_star.webp
│ │ └── version
│ │ │ ├── 20000.webp
│ │ │ ├── 20500.webp
│ │ │ └── 22000.webp
│ └── maimai
│ │ ├── class_rank
│ │ ├── 0.webp
│ │ ├── 1.webp
│ │ ├── 10.webp
│ │ ├── 11.webp
│ │ ├── 12.webp
│ │ ├── 13.webp
│ │ ├── 14.webp
│ │ ├── 15.webp
│ │ ├── 16.webp
│ │ ├── 17.webp
│ │ ├── 18.webp
│ │ ├── 19.webp
│ │ ├── 2.webp
│ │ ├── 20.webp
│ │ ├── 21.webp
│ │ ├── 22.webp
│ │ ├── 23.webp
│ │ ├── 24.webp
│ │ ├── 25.webp
│ │ ├── 3.webp
│ │ ├── 4.webp
│ │ ├── 5.webp
│ │ ├── 6.webp
│ │ ├── 7.webp
│ │ ├── 8.webp
│ │ └── 9.webp
│ │ ├── course_rank
│ │ ├── 0.webp
│ │ ├── 1.webp
│ │ ├── 10.webp
│ │ ├── 11.webp
│ │ ├── 12.webp
│ │ ├── 13.webp
│ │ ├── 14.webp
│ │ ├── 15.webp
│ │ ├── 16.webp
│ │ ├── 17.webp
│ │ ├── 18.webp
│ │ ├── 19.webp
│ │ ├── 2.webp
│ │ ├── 20.webp
│ │ ├── 21.webp
│ │ ├── 22.webp
│ │ ├── 23.webp
│ │ ├── 3.webp
│ │ ├── 4.webp
│ │ ├── 5.webp
│ │ ├── 6.webp
│ │ ├── 7.webp
│ │ ├── 8.webp
│ │ └── 9.webp
│ │ ├── dx_score
│ │ ├── 1.webp
│ │ ├── 2.webp
│ │ └── 3.webp
│ │ ├── icon_star.webp
│ │ ├── music_icon
│ │ ├── ap.webp
│ │ ├── app.webp
│ │ ├── blank.webp
│ │ ├── fc.webp
│ │ ├── fcp.webp
│ │ ├── fs.webp
│ │ ├── fsd.webp
│ │ ├── fsdp.webp
│ │ ├── fsp.webp
│ │ └── sync.webp
│ │ ├── music_rank
│ │ ├── a.webp
│ │ ├── aa.webp
│ │ ├── aaa.webp
│ │ ├── b.webp
│ │ ├── bb.webp
│ │ ├── bbb.webp
│ │ ├── c.webp
│ │ ├── d.webp
│ │ ├── s.webp
│ │ ├── sp.webp
│ │ ├── ss.webp
│ │ ├── ssp.webp
│ │ ├── sss.webp
│ │ └── sssp.webp
│ │ └── version
│ │ ├── 20000.webp
│ │ ├── 21000.webp
│ │ ├── 22000.webp
│ │ ├── 23000.webp
│ │ └── 24000.webp
├── docs
│ ├── about.md
│ ├── api
│ │ ├── chunithm.md
│ │ └── maimai.md
│ ├── changelog.md
│ ├── faq.md
│ ├── index.md
│ ├── lxbot.md
│ ├── privacy-policy.md
│ ├── settings.md
│ ├── sync.md
│ └── terms-of-use.md
├── empty.webp
├── error.webp
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── favicon.webp
├── logo.webp
├── product
│ ├── akihabot.webp
│ ├── findmaimaidx.webp
│ ├── lxbot.webp
│ ├── maiproberplus.webp
│ └── youmubot.webp
└── robots.txt
├── src
├── App.module.css
├── App.tsx
├── components
│ ├── Alias
│ │ ├── Alias.module.css
│ │ ├── Alias.tsx
│ │ ├── AliasButton.tsx
│ │ ├── AliasList.tsx
│ │ ├── AliasModal.tsx
│ │ └── CreateAliasModal.tsx
│ ├── AudioPlayer.tsx
│ ├── Home
│ │ ├── Product.module.css
│ │ ├── Product.tsx
│ │ └── ProductCarousel.tsx
│ ├── LoginAlert.tsx
│ ├── Marquee.tsx
│ ├── Page
│ │ ├── Page.tsx
│ │ ├── PageHeader.module.css
│ │ ├── PageHeader.tsx
│ │ ├── PageRawContent.module.css
│ │ ├── PageRawContent.tsx
│ │ ├── PageTabs.module.css
│ │ └── PageTabs.tsx
│ ├── Plates
│ │ ├── PlateCombobox.tsx
│ │ └── RequiredSong.tsx
│ ├── Profile
│ │ ├── PlayerPanel
│ │ │ ├── NotFoundAlert.tsx
│ │ │ ├── PlayerModal.module.css
│ │ │ ├── PlayerModal.tsx
│ │ │ ├── PlayerPanel.module.css
│ │ │ ├── PlayerPanel.tsx
│ │ │ ├── Skeleton.tsx
│ │ │ ├── chunithm
│ │ │ │ ├── PlayerContent.tsx
│ │ │ │ ├── PlayerModal.tsx
│ │ │ │ └── RatingTrend.tsx
│ │ │ └── maimai
│ │ │ │ ├── PlayerContent.tsx
│ │ │ │ ├── PlayerModal.tsx
│ │ │ │ └── RatingTrend.tsx
│ │ ├── PlayerSection.tsx
│ │ ├── Profile.module.css
│ │ ├── UserBindSection.tsx
│ │ ├── UserSection.tsx
│ │ └── UserTokenSection.tsx
│ ├── RadioCardGroup.module.css
│ ├── RadioCardGroup.tsx
│ ├── RouterTransition.tsx
│ ├── Scores
│ │ ├── AdvancedFilter.tsx
│ │ ├── ChartComment.module.css
│ │ ├── ChartComment.tsx
│ │ ├── RatingHistoryModal.tsx
│ │ ├── RatingSegments.module.css
│ │ ├── RatingSegments.tsx
│ │ ├── ScoreHistory.tsx
│ │ ├── ScoreList.tsx
│ │ ├── ScoreModal.module.css
│ │ ├── ScoreModal.tsx
│ │ ├── ScoreModalMenu.module.css
│ │ ├── ScoreModalMenu.tsx
│ │ ├── ScoreRanking.tsx
│ │ ├── Scores.module.css
│ │ ├── chunithm
│ │ │ ├── Chart.tsx
│ │ │ ├── CreateScoreModal.tsx
│ │ │ ├── Score.tsx
│ │ │ ├── ScoreHistory.tsx
│ │ │ ├── ScoreModal.tsx
│ │ │ └── StatisticsSection.tsx
│ │ └── maimai
│ │ │ ├── Chart.tsx
│ │ │ ├── CreateScoreModal.tsx
│ │ │ ├── DeluxeRatingCalculator.module.css
│ │ │ ├── DeluxeRatingCalculator.tsx
│ │ │ ├── DeluxeScoreDetail.tsx
│ │ │ ├── Score.tsx
│ │ │ ├── ScoreHistory.tsx
│ │ │ ├── ScoreModal.tsx
│ │ │ ├── StatisticsSection.module.css
│ │ │ └── StatisticsSection.tsx
│ ├── Settings
│ │ ├── SettingList.tsx
│ │ ├── Settings.module.css
│ │ └── SettingsModal.tsx
│ ├── Shell
│ │ ├── Footer
│ │ │ ├── Footer.module.css
│ │ │ └── Footer.tsx
│ │ ├── Header
│ │ │ ├── ColorSchemeToggle.tsx
│ │ │ ├── GameTabs.module.css
│ │ │ ├── GameTabs.tsx
│ │ │ ├── Header.module.css
│ │ │ ├── Header.tsx
│ │ │ └── Logo.tsx
│ │ ├── Navbar
│ │ │ ├── Navbar.module.css
│ │ │ ├── Navbar.tsx
│ │ │ └── NavbarButton.tsx
│ │ ├── Shell.module.css
│ │ └── Shell.tsx
│ ├── SongCombobox.tsx
│ ├── SongDisabledIndicator.tsx
│ ├── Songs
│ │ ├── SongCard.module.css
│ │ ├── SongCard.tsx
│ │ ├── SongDifficulty.module.css
│ │ ├── SongDifficultyList.tsx
│ │ ├── chunithm
│ │ │ └── SongDifficulty.tsx
│ │ └── maimai
│ │ │ └── SongDifficulty.tsx
│ ├── Sync
│ │ ├── CopyButtonWithIcon.tsx
│ │ ├── CrawlTokenAlert.tsx
│ │ ├── ScoresChangesModal.tsx
│ │ └── WechatOAuthLink.tsx
│ ├── Users
│ │ ├── EditUserModal.tsx
│ │ └── SendBatchEmailModal.tsx
│ └── YearInReview
│ │ ├── SongRankingSection.module.css
│ │ ├── SongRankingSection.tsx
│ │ ├── SongTimelineSection.module.css
│ │ ├── SongTimelineSection.tsx
│ │ ├── TagRadarSection.tsx
│ │ ├── UploadRhythmSection.tsx
│ │ ├── YearSummarySection.module.css
│ │ └── YearSummarySection.tsx
├── data
│ ├── products.json
│ └── tags.json
├── hooks
│ ├── swr
│ │ ├── fetcher.ts
│ │ ├── useAliasVotes.ts
│ │ ├── useAliases.ts
│ │ ├── useBests.ts
│ │ ├── usePlayer.ts
│ │ ├── useScores.ts
│ │ ├── useSiteConfig.ts
│ │ ├── useUser.ts
│ │ ├── useUserConfig.ts
│ │ ├── useUserToken.ts
│ │ └── useYearInReview.ts
│ ├── useAliasListStore.ts
│ ├── useFixedGame.ts
│ ├── useGame.ts
│ ├── useShellViewportSize.ts
│ ├── useSongListStore.ts
│ └── useVersionChecker.tsx
├── index.css
├── main.tsx
├── pages
│ ├── Form.module.css
│ ├── Page.module.css
│ ├── admin
│ │ └── Panel
│ │ │ ├── Panel.tsx
│ │ │ ├── developers
│ │ │ ├── AdminDevelopersSection.module.css
│ │ │ └── AdminDevelopersSection.tsx
│ │ │ ├── index.ts
│ │ │ └── users
│ │ │ └── AdminUsersSection.tsx
│ ├── alias
│ │ └── Vote.tsx
│ ├── developer
│ │ ├── Apply.tsx
│ │ └── Info.tsx
│ ├── public
│ │ ├── Docs.module.css
│ │ ├── Docs.tsx
│ │ ├── ErrorPage.module.css
│ │ ├── Fallback.tsx
│ │ ├── ForgotPassword.tsx
│ │ ├── Home.module.css
│ │ ├── Home.tsx
│ │ ├── Login.tsx
│ │ ├── NotFound.tsx
│ │ ├── Register.tsx
│ │ ├── ResetPassword.tsx
│ │ ├── YearInReview.module.css
│ │ └── YearInReview.tsx
│ └── user
│ │ ├── Plates.tsx
│ │ ├── Profile.tsx
│ │ ├── Scores
│ │ ├── Scores.tsx
│ │ ├── backup
│ │ │ ├── ScoreBackupSection.module.css
│ │ │ └── ScoreBackupSection.tsx
│ │ ├── bests
│ │ │ └── ScoreBestsSection.tsx
│ │ ├── index.ts
│ │ └── list
│ │ │ └── ScoreListSection.tsx
│ │ ├── Settings.tsx
│ │ ├── Songs.tsx
│ │ ├── Sync.module.css
│ │ └── Sync.tsx
├── router.tsx
├── types
│ ├── alias.d.ts
│ ├── game.d.ts
│ ├── player.d.ts
│ ├── score.d.ts
│ └── user.d.ts
├── utils
│ ├── api
│ │ ├── alias.ts
│ │ ├── api.ts
│ │ ├── comment.ts
│ │ ├── developer.ts
│ │ ├── misc.ts
│ │ ├── player.ts
│ │ ├── song
│ │ │ ├── chunithm.ts
│ │ │ ├── maimai.ts
│ │ │ └── song.tsx
│ │ └── user.ts
│ ├── checkProxy.ts
│ ├── color.ts
│ ├── context.ts
│ ├── modal.tsx
│ ├── reCaptcha.ts
│ ├── session.ts
│ └── validator.ts
├── vite-env.d.ts
└── wdyr.js
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── yarn.lock
/.env.production:
--------------------------------------------------------------------------------
1 | VITE_API_URL=https://maimai.lxns.net/api/v0
2 | VITE_ASSET_URL=https://assets.lxns.net
3 | VITE_RECAPTCHA_SITE_KEY=6LefxhIjAAAAADI0_XvRZmguDUharyWf3kGFhxqX
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/
127 | .pnp.*
128 |
129 | # JetBrains IDEs
130 | .idea
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["stylelint-config-standard-scss"],
3 | "rules": {
4 | "custom-property-pattern": null,
5 | "selector-class-pattern": null,
6 | "scss/no-duplicate-mixins": null,
7 | "declaration-empty-line-before": null,
8 | "declaration-block-no-redundant-longhand-properties": null,
9 | "alpha-value-notation": null,
10 | "custom-property-empty-line-before": null,
11 | "property-no-vendor-prefix": null,
12 | "color-function-notation": null,
13 | "length-zero-no-unit": null,
14 | "selector-not-notation": null,
15 | "no-descending-specificity": null,
16 | "comment-empty-line-before": null,
17 | "scss/at-mixin-pattern": null,
18 | "scss/at-rule-no-unknown": null,
19 | "value-keyword-case": null,
20 | "media-feature-range-notation": null,
21 | "selector-pseudo-class-no-unknown": [
22 | true,
23 | {
24 | "ignorePseudoClasses": ["global"]
25 | }
26 | ]
27 | }
28 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | maimai DX 查分器
4 |
5 |
6 | 
7 | 
8 | 
9 | 
10 |
11 | 一个简单的舞萌 DX & 中二节奏国服查分器,玩家可以查看并管理自己的成绩,同时也有公共的 API 接口供开发者获取玩家的成绩数据。
12 |
13 | 该项目为查分器的前端部分,使用 React + TypeScript + Vite 编写。
14 |
15 | 了解查分器的使用方法,请参见:[官方文档](https://maimai.lxns.net/docs)
16 |
17 | ## 特性
18 |
19 | - 📱 支持「舞萌 DX」与「中二节奏」的游戏数据同步
20 | - 💻 不受设备限制,支持多平台同步、管理游戏数据
21 | - 🚀 快速的游戏数据同步体验
22 | - 🤩 易于使用的成绩、曲目与姓名框查询功能
23 | - 🌐 开放的 API 接口,支持第三方开发者对接
24 | - 🌈 支持暗色与自动主题
25 |
26 | ## 调试
27 |
28 | ```bash
29 | npm run dev
30 | ```
31 |
32 | ## 构建
33 |
34 | ```bash
35 | npm run build
36 | ```
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | maimai DX 查分器
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'postcss-preset-mantine': {
4 | autoRem: true,
5 | },
6 | 'postcss-simple-vars': {
7 | variables: {
8 | 'mantine-breakpoint-xs': '36em',
9 | 'mantine-breakpoint-sm': '48em',
10 | 'mantine-breakpoint-md': '62em',
11 | 'mantine-breakpoint-lg': '75em',
12 | 'mantine-breakpoint-xl': '88em',
13 | },
14 | },
15 | },
16 | };
--------------------------------------------------------------------------------
/public/assets/chunithm/character/copper.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/copper.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/character/gold.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/gold.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/character/holographic.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/holographic.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/character/normal.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/normal.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/character/platina.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/platina.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/character/rainbow.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/rainbow.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/character/silver.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/silver.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/class_emblem/base/1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/base/1.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/class_emblem/base/2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/base/2.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/class_emblem/base/3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/base/3.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/class_emblem/base/4.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/base/4.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/class_emblem/base/5.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/base/5.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/class_emblem/base/6.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/base/6.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/class_emblem/medal/1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/medal/1.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/class_emblem/medal/2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/medal/2.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/class_emblem/medal/3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/medal/3.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/class_emblem/medal/4.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/medal/4.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/class_emblem/medal/5.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/medal/5.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/class_emblem/medal/6.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/medal/6.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/absolute.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/absolute.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/absolutep.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/absolutep.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/absolutepp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/absolutepp.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/alljustice.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/alljustice.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/alljustice_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/alljustice_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/alljustice_xs.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/alljustice_xs.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/alljusticecritical.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/alljusticecritical.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/alljusticecritical_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/alljusticecritical_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/alljusticecritical_xs.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/alljusticecritical_xs.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/blank_xs.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/blank_xs.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/catastrophy.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/catastrophy.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/clear.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/clear.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/clear_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/clear_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/failed.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/failed.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/fullchain.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullchain.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/fullchain2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullchain2.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/fullchain2_xs.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullchain2_xs.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/fullchain_blank.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullchain_blank.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/fullchain_xs.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullchain_xs.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/fullcombo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullcombo.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/fullcombo_blank.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullcombo_blank.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/fullcombo_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullcombo_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/fullcombo_xs.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullcombo_xs.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_icon/hard.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/hard.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/a.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/a.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/a_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/a_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/aa.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/aa.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/aa_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/aa_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/aaa.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/aaa.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/aaa_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/aaa_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/b.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/b.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/b_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/b_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/bb.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/bb.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/bb_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/bb_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/bbb.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/bbb.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/bbb_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/bbb_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/c.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/c.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/c_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/c_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/d.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/d.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/d_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/d_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/s_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/s_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/sp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/sp.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/sp_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/sp_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/ss.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/ss.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/ss_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/ss_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/ssp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/ssp.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/ssp_s.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/ssp_s.png
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/ssp_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/ssp_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/sss.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/sss.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/sss_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/sss_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/sssp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/sssp.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/music_rank/sssp_s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/sssp_s.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/reborn_star.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/reborn_star.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/version/20000.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/version/20000.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/version/20500.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/version/20500.webp
--------------------------------------------------------------------------------
/public/assets/chunithm/version/22000.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/version/22000.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/0.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/0.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/1.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/10.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/10.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/11.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/11.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/12.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/12.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/13.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/13.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/14.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/14.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/15.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/15.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/16.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/16.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/17.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/17.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/18.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/18.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/19.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/19.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/2.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/20.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/20.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/21.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/21.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/22.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/22.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/23.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/23.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/24.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/24.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/25.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/25.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/3.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/4.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/4.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/5.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/5.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/6.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/6.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/7.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/7.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/8.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/8.webp
--------------------------------------------------------------------------------
/public/assets/maimai/class_rank/9.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/9.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/0.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/0.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/1.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/10.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/10.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/11.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/11.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/12.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/12.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/13.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/13.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/14.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/14.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/15.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/15.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/16.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/16.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/17.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/17.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/18.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/18.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/19.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/19.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/2.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/20.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/20.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/21.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/21.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/22.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/22.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/23.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/23.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/3.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/4.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/4.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/5.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/5.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/6.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/6.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/7.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/7.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/8.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/8.webp
--------------------------------------------------------------------------------
/public/assets/maimai/course_rank/9.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/9.webp
--------------------------------------------------------------------------------
/public/assets/maimai/dx_score/1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/dx_score/1.webp
--------------------------------------------------------------------------------
/public/assets/maimai/dx_score/2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/dx_score/2.webp
--------------------------------------------------------------------------------
/public/assets/maimai/dx_score/3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/dx_score/3.webp
--------------------------------------------------------------------------------
/public/assets/maimai/icon_star.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/icon_star.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_icon/ap.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/ap.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_icon/app.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/app.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_icon/blank.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/blank.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_icon/fc.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/fc.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_icon/fcp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/fcp.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_icon/fs.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/fs.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_icon/fsd.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/fsd.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_icon/fsdp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/fsdp.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_icon/fsp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/fsp.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_icon/sync.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/sync.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/a.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/a.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/aa.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/aa.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/aaa.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/aaa.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/b.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/b.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/bb.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/bb.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/bbb.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/bbb.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/c.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/c.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/d.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/d.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/s.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/s.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/sp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/sp.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/ss.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/ss.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/ssp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/ssp.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/sss.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/sss.webp
--------------------------------------------------------------------------------
/public/assets/maimai/music_rank/sssp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/sssp.webp
--------------------------------------------------------------------------------
/public/assets/maimai/version/20000.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/version/20000.webp
--------------------------------------------------------------------------------
/public/assets/maimai/version/21000.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/version/21000.webp
--------------------------------------------------------------------------------
/public/assets/maimai/version/22000.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/version/22000.webp
--------------------------------------------------------------------------------
/public/assets/maimai/version/23000.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/version/23000.webp
--------------------------------------------------------------------------------
/public/assets/maimai/version/24000.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/version/24000.webp
--------------------------------------------------------------------------------
/public/docs/about.md:
--------------------------------------------------------------------------------
1 | # 关于
2 |
3 | ---
4 |
5 | [落雪咖啡屋 maimai DX 查分器](/)是一个简单的舞萌 DX & 中二节奏国服查分器,玩家可以查看并管理自己的成绩,同时也有公共的 API 接口供开发者获取玩家的成绩数据。
6 |
7 | ## 联系我们
8 |
9 | QQ 群:
10 | - 用户交流群:**597413470**
11 | - 开发者交流群:**991669419**
12 |
13 | GitHub:[@Lxns-Network](https://github.com/Lxns-Network)
14 |
15 | 哔哩哔哩:[@软糖酱_Official](https://space.bilibili.com/1432317833)
16 |
17 | ## 开放源代码
18 |
19 | 查分器前端的源代码是开放的,您可以在 [GitHub](https://github.com/Lxns-Network/maimai-prober-frontend) 上查看并下载源代码。
20 |
21 | ## 赞助
22 |
23 | 查分器的维护及运营需要费用,您可以通过[爱发电](https://ifdian.net/a/lxnssama)支持查分器的可持续运行。
--------------------------------------------------------------------------------
/public/docs/faq.md:
--------------------------------------------------------------------------------
1 | # 常见问题
2 |
3 | ---
4 |
5 | 仍然没有找到答案?请[联系我们](/docs/about#联系我们)寻求帮助。
6 |
7 | ## 查分器相关问题
8 |
9 | ### 如果我忘记邮箱与密码,我该如何找回?
10 |
11 | 请[联系我们](/docs/about#联系我们)提供有关你账号的其它信息,我们会协助你找回账号。
12 |
13 | ### 为什么我微信里提示“上传成功”,但是查询成绩的时候发现并没有上传?
14 |
15 | 
16 |
17 | 因为微信中的“上传成功”指的是你的**玩家信息**(如玩家名、好友码、DX Rating 等)已经提交到查分器了,但是成绩爬取是在你提交后进行的。成绩爬取所需要的时间由当时 NET 的访问速度(通常晚上耗时较长),以及你的爬取设置所决定。
18 |
19 | 查询成绩爬取的进度与结果,你需要前往查分器,并按照查分器的步骤进行。针对如何使用查分器进行传分,请参考[同步游戏数据](/docs/sync)中的**在线同步**。
20 |
21 | ### 我没有注册查分器账号,能否使用第三方开发者提供的服务同步游戏数据?
22 |
23 | 可以的,但得注意第三方开发者是否支持使用好友码上传游戏数据。
24 |
25 | ### 为什么部分成绩不会显示在游玩历史记录中?
26 |
27 | 游玩历史记录只会显示包含游玩时间(`play_time`)的成绩,如果你的成绩没有游玩时间,那么该成绩将不会显示在游玩历史记录中。
28 |
29 | #### 舞萌 DX
30 |
31 | 使用查分器爬取舞萌 DX 的成绩时,你需要在[账号设置](/user/settings)中将**爬取谱面成绩的方式**设置为“自动检测”或“增量爬取”,以确保成绩能够爬取到游玩时间。
32 |
33 | #### 中二节奏
34 |
35 | 中二节奏默认会爬取成绩的游玩时间,但是 WORLD'S END 难度的成绩不包含游玩时间:[为什么 WORLD'S END 难度的成绩不会出现在 Recent 50 中?](#为什么-worlds-end-难度的成绩不会出现在-recent-50-中)
36 |
37 | ### 如果我的舞萌 DX 账号被警告了,我还能同步游戏数据吗?
38 |
39 | 
40 |
41 | 可以,我们会自动同意条款并跳过警告页面,以便你能够继续同步游戏数据。
42 |
43 | ::: warning 注意
44 | 不包含被封禁的情况,如果你的账号被封禁,查分器将无法同步你的游戏数据。
45 | :::
46 |
47 | ### 为什么爬取中二节奏的成绩时,提示“无法获取到好友码,请稍后再试”?
48 |
49 | 请确保你在最新版本的中二节奏有游玩记录,并且能正常进入 CHUNITHM-NET,而不是提示错误。
50 |
51 | ### 为什么 WORLD'S END 难度的成绩不会出现在 Recent 50 中?
52 |
53 | 由于 NET 返回 WORLD'S END 难度的成绩时,没有包含谱面的难度信息,查分器无法准确判断该成绩是哪个难度的谱面。故在爬取时无法获取到游玩时间,所以无法将该成绩加入 Recent 50。
54 |
55 | ## LxBot 相关问题
56 |
57 | ### 为什么我的 Best 50 没有姓名框和背景?
58 |
59 | 请前往查分器的[账号设置](/user/settings),将“允许爬取姓名框”与“允许爬取背景”打开,并重新进行爬取操作。
60 |
61 | ### 为什么我查询 Best 50 会提示“成绩有误或同步不完全”?
62 |
63 | 
64 |
65 | 请前往查分器的[账号设置](/user/settings),将**爬取谱面成绩的方式**设置为“自动检测”,该选项会自动检测并使用最适合的爬取方式。
66 |
67 | 如果执行完上述操作后仍然无法解决问题,请将“允许覆盖最佳成绩”打开,并重新进行完整爬取操作。
68 |
69 | ::: warning 注意
70 | 手动创建错误成绩,或是通过其他方式导入了错误成绩,均会导致查分器计算出的 Best 50 与玩家 DX Rating 不一致。
71 | :::
72 |
--------------------------------------------------------------------------------
/public/docs/index.md:
--------------------------------------------------------------------------------
1 | # 帮助文档
2 |
3 | ---
4 |
5 | 欢迎来到[落雪咖啡屋 maimai DX 查分器](/)的帮助文档!
6 |
7 | 在这里,你可以作为用户查阅基本的查分器使用方法,也可以作为开发者了解如何对接查分器。
8 |
9 | ## 特性
10 |
11 | - 📱 支持「舞萌 DX」与「中二节奏」的游戏数据同步
12 | - 💻 不受设备限制,支持多平台同步、管理游戏数据
13 | - 🚀 快速的游戏数据同步体验
14 | - 🤩 易于使用的成绩、曲目与姓名框查询功能
15 | - 🌐 开放的 API 接口,支持第三方开发者对接
16 | - 🌈 支持暗色与自动主题
17 |
18 | ## 用户使用手册
19 |
20 | - [同步游戏数据](/docs/sync)
21 | - [账号设置](/docs/settings)
22 | - [使用 LxBot 查询成绩](/docs/lxbot)
23 | - [常见问题](/docs/faq)
24 |
25 | ## 开发者文档
26 |
27 | - [舞萌 DX API 文档](/docs/api/maimai)
28 | - [中二节奏 API 文档](/docs/api/chunithm)
29 |
30 | ## 其它
31 |
32 | - [关于](/docs/about)
33 | - [服务条款](/docs/terms-of-use)
34 | - [隐私政策](/docs/privacy-policy)
35 | - [更新日志](/docs/changelog)
--------------------------------------------------------------------------------
/public/docs/lxbot.md:
--------------------------------------------------------------------------------
1 | # 使用 LxBot 查询成绩
2 |
3 | ---
4 |
5 | ## 通过 QQ 官方机器人查询(推荐)
6 |
7 | 使用 QQ 扫描下方二维码,将 LxBot 添加到你所管理的 QQ 群。
8 |
9 | 
10 |
11 | 由于官方机器人无法直接获取到你的 QQ 号,你需要使用指令绑定你的游戏数据:
12 | - 舞萌 DX:`/mai bind <好友码>`
13 | - 中二节奏:`/chu bind <好友码>`
14 |
15 | 绑定完成后使用指令查询你的游戏成绩:
16 | - 舞萌 DX Best 50:`/mai b50`
17 | - 中二节奏 Best 30:`/chu b30`
--------------------------------------------------------------------------------
/public/docs/privacy-policy.md:
--------------------------------------------------------------------------------
1 | # 隐私政策
2 |
3 | > 最后更新日期:2024/1/30
4 |
5 | ---
6 |
7 | 若要使用[落雪咖啡屋 maimai DX 查分器](/)(以下简称本网站)的服务,请您务必仔细阅读并透彻理解本声明。
8 |
9 | 请注意,访问和使用本网站,您的使用行为将被视为对本声明全部内容的认可。如果您不同意本声明的任何内容,请您立即停止使用本网站。
10 |
11 | ## 定义
12 |
13 | 为了使本条款更加清晰,我们使用缩短的术语来表达某些概念。
14 |
15 | - **本网站**指[落雪咖啡屋 maimai DX 查分器](/)。
16 | - **我们**指本网站的运营者 [Lxns Network](https://lxns.net)。
17 | - **您**指使用本网站的用户。
18 | - **舞萌DX**、**中二节奏**是由 SEGA 发行的音乐游戏。
19 | - **游戏数据**指您在上述游戏中产生的数据,包括但不限于您的好友码、玩家信息、玩家收藏品、游玩成绩等。
20 | - **开发者**指申请、对接本网站查分器 API 的第三方开发者。
21 |
22 | ## 信息收集
23 |
24 | - 您使用本网站时,我们会收集您的 IP 地址、浏览器信息、操作系统信息、访问时间等信息。
25 | - 您使用本网站提供的舞萌DX、中二节奏 NET 游戏数据爬取服务时,我们会收集您的游戏数据。
26 |
27 | ## 信息使用
28 |
29 | - 本网站收集的信息仅用于提供本网站的服务,不会用于其他用途。
30 | - 本网站会将您的信息用于生成排行榜、统计数据,但不会包含您的任何个人信息。
31 | - 本网站会向开发者提供您的游戏数据,用于提供本网站的服务,但不会包含您在本网站注册的用户信息。
32 | - 本网站不会将您的信息用于任何商业用途。
33 |
34 | ## 条款修改
35 |
36 | - 本条款是您在本网站签署的使用协议的组成部分之一,请您仔细阅读。
37 | - 当条款发生变更时,我们会在本页面上发布通知。如果您在条款修改后继续使用本网站提供的服务,即表示您同意并接受修改后的条款。
38 | - Lxns Network 保留对本条款作出不定时修改的权利。
39 | - Lxns Network 对本页面内容拥有最终解释权。
40 |
--------------------------------------------------------------------------------
/public/docs/settings.md:
--------------------------------------------------------------------------------
1 | # 账号设置
2 |
3 | ---
4 |
5 | ## 爬取数据
6 |
7 | ### 舞萌 DX
8 |
9 | #### 爬取谱面成绩的方式
10 |
11 | 设置每次爬取时使用的爬取方式,增量爬取依赖最近游玩记录,适合已经完整爬取后频繁爬取,更加稳定。
12 |
13 | > 当爬取方式为自动检测时,会先尝试使用增量爬取。爬取后若检测到玩家 DX Rating 与成绩不一致时,会使用完整爬取重新爬取一次。
14 |
15 | | 特性 | 自动检测 | 完整爬取 | 增量爬取 |
16 | |-|-|-|-|
17 | | 成绩爬取数量 | - | 全部 | 最多 50 条 |
18 | | 爬取游玩时间 | ✔️ | ❌ | ✔️ |
19 | | 爬取非最佳成绩 | ✔️ | ❌ | ✔️ |
20 | | 爬取速度 | - | 慢 | 相对较快 |
21 | | 成绩来源 | - | 最佳成绩 | 最近游玩记录 |
22 |
23 | ::: warning 注意
24 | 完整爬取爬取的是**历史最佳** Full Combo、Full Sync、DX 分数(下称参数),并非增量爬取爬取的当前成绩的真实参数。在部分情况下,自动检测可能会出现来自不同爬取方式的同一个成绩(但参数不一致)。
25 | :::
26 |
27 | ### 通用选项
28 |
29 | #### 允许覆盖最佳成绩
30 |
31 | 允许后,每次“完整爬取”或通过第三方开发者写入时会检查成绩是否低于最佳成绩,低于则覆盖最佳成绩。
32 |
33 | ##### 适用场景
34 |
35 | - 你导入了其他玩家的成绩(如通过第三方开发者)
36 | - 你手动创建了错误成绩
37 |
38 | ::: warning 注意
39 | 目前不支持使用空成绩(即在 NET 中没有显示的成绩)覆盖最佳成绩。达成率 0% 的成绩可以覆盖最佳成绩。
40 | :::
--------------------------------------------------------------------------------
/public/docs/sync.md:
--------------------------------------------------------------------------------
1 | # 同步游戏数据
2 |
3 | ---
4 |
5 | ## 同步方法
6 |
7 | 针对不同需求,你可以选择不同的同步方法。
8 |
9 | | 特性 | 在线同步 | 离线同步 |
10 | |-|-|-|
11 | | 绑定查分器账号 | ✔️ | ❌ |
12 | | 查询同步结果 | ✔️ | ❌ |
13 | | 同步超时时间 | 10 分钟 | 10 分钟 |
14 | | OAuth 链接固定不变 | ❌ | ✔️ |
15 | | OAuth 链接有效期 | 15 分钟 | 长期有效 |
16 |
17 | ### 在线同步(查分器网页)
18 |
19 | 在线同步需要注册并登录查分器账号,在[同步游戏数据页](/user/sync)跟随页面内指引,正确配置 HTTP 代理并获取 OAuth 链接(链接中通常带 `token` 参数)。
20 |
21 | 适用于用户初次同步,将玩家数据绑定到查分器账号。
22 |
23 | ### 离线同步
24 |
25 | 离线同步不需要登录查分器账号,且 OAuth 链接通常固定不变。
26 |
27 | 如果先前已经将玩家数据绑定到查分器账号,则查分器账号中的设置也会在离线同步中生效。
28 |
29 | ## 同步步骤
30 |
31 | ::: info 提示
32 | 可观看同步教程视频:[BV1mz421z7pg](https://www.bilibili.com/video/BV1mz421z7pg)
33 | :::
34 |
35 | ### 一、配置 HTTP 代理
36 |
37 | #### Windows 11
38 |
39 | 打开系统设置中的代理(或在浏览器地址栏输入 `ms-settings:network-proxy` 快捷打开):
40 |
41 | 
42 |
43 | 输入以下代理服务器地址,启用代理并保存:
44 |
45 | ```
46 | proxy.maimai.lxns.net:8080
47 | ```
48 |
49 | 
50 |
51 | #### Android
52 |
53 | ##### 通过接入点名称(APN)配置
54 |
55 | 在设置中搜索“接入点名称”,并新建接入点:
56 |
57 | 
58 |
59 | 编辑以下参数(未提及的参数无需修改):
60 |
61 | - 名称:随意
62 | - APN(根据 SIM 卡的运营商填写):
63 | - 中国移动:`cmnet`
64 | - 中国电信:`ctnet`
65 | - 中国联通:`3gnet`
66 | - 中国广电:`cbnet`
67 | > 仅列举常见运营商,其它运营商请自行搜索 APN 的值,或参考手机已有 APN 修改。
68 | - 代理服务器(代理):`proxy.maimai.lxns.net`
69 | - 端口:`8080`
70 |
71 | 保存并选中新建的 APN,同步完成后记得选回原有 APN,否则无法正常联网。
72 |
73 | ::: info 仍然无法同步?
74 | 选中新建的 APN 后尝试重启手机。
75 | :::
76 |
77 | #### iOS 或 iPadOS
78 |
79 | ##### 通过无线局域网配置
80 |
81 | 在无线局域网设置中查看当前连接 WLAN 的详情:
82 |
83 | 
84 |
85 | 在二级页面找到 HTTP 代理:
86 |
87 | 
88 |
89 | 编辑代理配置为手动,输入以下代理服务器地址,并存储:
90 |
91 | ```
92 | proxy.maimai.lxns.net:8080
93 | ```
94 |
95 | 
96 |
97 | #### Clash 或 Shadowrocket
98 |
99 | 通过 URL 导入订阅:
100 |
101 | ```
102 | https://maimai.lxns.net/api/v0/proxy-config/clash
103 | ```
104 |
105 | 代理选中“maimai DX 查分器代理”,并启用系统代理。
106 |
107 | ### 二、使用微信打开 OAuth 链接
108 |
109 | #### 在线同步获取
110 |
111 | 正确配置代理后,在[同步游戏数据页](/user/sync)选择爬取的游戏并直接复制 OAuth 链接(带 `token`)。
112 |
113 | #### 离线同步获取
114 |
115 | 根据需要选择对应游戏的 OAuth 链接:
116 |
117 | - 舞萌 DX:
118 | ```
119 | https://maimai.lxns.net/api/v0/maimai/wechat/auth
120 | ```
121 |
122 | - 中二节奏:
123 | ```
124 | https://maimai.lxns.net/api/v0/chunithm/wechat/auth
125 | ```
126 |
127 | 在聊天中输入 OAuth 链接并发送至安全的聊天(如文件传输助手),直接点击链接打开网页。
128 |
129 | ::: warning 注意
130 | 不要将 OAuth 链接粘贴到搜索框打开,否则可能会导致 OAuth 链接失效。
131 | :::
132 |
133 | #### 确认是否开始同步
134 |
135 | 若页面显示为如下内容,则代表你的玩家数据已经被上传至服务器进行处理:
136 |
137 | 
138 |
139 | 若页面提示网络出错,请检查代理配置是否正确:
140 |
141 | 
142 |
143 | ### 三、等待数据同步完成(仅在线同步)
144 |
145 | ::: info 提示
146 | 离线同步不支持查询数据同步状态。
147 | :::
148 |
149 | 同步完成后,返回同步游戏数据页查询成绩爬取状态。届时,你的玩家数据与成绩将会被同步到查分器,并与你的查分器账号绑定。
150 |
--------------------------------------------------------------------------------
/public/docs/terms-of-use.md:
--------------------------------------------------------------------------------
1 | # 服务条款
2 |
3 | > 最后更新日期:2024/1/30
4 |
5 | ---
6 |
7 | 若要使用[落雪咖啡屋 maimai DX 查分器](/)(以下简称本网站)的服务,请您务必仔细阅读并透彻理解本声明。
8 |
9 | 请注意,访问和使用本网站,您的使用行为将被视为对本声明全部内容的认可。如果您不同意本声明的任何内容,请您立即停止使用本网站。
10 |
11 | ## 定义
12 |
13 | 为了使本条款更加清晰,我们使用缩短的术语来表达某些概念。
14 |
15 | - **本网站**指[落雪咖啡屋 maimai DX 查分器](/)。
16 | - **我们**指本网站的运营者 [Lxns Network](https://lxns.net)。
17 | - **您**指使用本网站的用户。
18 | - **舞萌DX**、**中二节奏**是由 SEGA 发行的音乐游戏。
19 | - **游戏数据**指您在上述游戏中产生的数据,包括但不限于您的好友码、玩家信息、玩家收藏品、游玩成绩等。
20 | - **开发者**指申请、对接本网站查分器 API 的第三方开发者。
21 |
22 | ## 服务内容
23 |
24 | - 本网站提供的服务内容包括但不限于为用户提供舞萌DX、中二节奏 NET 游戏数据爬取、查询、管理等服务。
25 | - 本网站提供的服务仅供个人学习、研究或欣赏使用,您不得将本网站提供的服务用于商业用途。
26 | - 本网站提供的服务内容可能会随时变更,本网站不承诺对用户提供任何形式的通知。
27 |
28 | ## 用户行为
29 |
30 | - 您在使用本网站提供的服务时,应遵守中华人民共和国相关法律法规,不得利用本网站提供的服务从事任何违法违规行为。
31 | - 您不得利用本网站提供的服务干扰本网站的正常运行,包括但不限于利用本网站提供的服务对本网站的服务器进行攻击、利用本网站提供的服务对本网站的数据进行篡改等。
32 | - 开发者不得利用本网站提供的服务从事任何违法违规行为,包括但不限于利用本网站提供的服务通过任何方式获取用户的个人信息。
33 | - 开发者不得将本网站提供的服务,以及从本网站提供的服务获取的游戏数据用于任何商业用途。
34 | - 对于任何违反上述协议的行为,本网站有权采取措施,包括但不限于限制、禁止您使用本网站提供的服务。
35 |
36 | ## 免责声明
37 |
38 | 您已知晓并同意,您使用本网站提供的服务存在违反[《舞萌&中二 游戏条款》](http://wc.wahlap.net/sega/music/terms/index.html)的风险,本网站不对您使用本网站提供的服务所产生的后果承担任何责任。
39 |
40 | ## 条款修改
41 |
42 | - 本条款是您在本网站签署的使用协议的组成部分之一,请您仔细阅读。
43 | - 当条款发生变更时,我们会在本页面上发布通知。如果您在条款修改后继续使用本网站提供的服务,即表示您同意并接受修改后的条款。
44 | - Lxns Network 保留对本条款作出不定时修改的权利。
45 | - Lxns Network 对本页面内容拥有最终解释权。
46 |
--------------------------------------------------------------------------------
/public/empty.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/empty.webp
--------------------------------------------------------------------------------
/public/error.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/error.webp
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/favicon.webp
--------------------------------------------------------------------------------
/public/logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/logo.webp
--------------------------------------------------------------------------------
/public/product/akihabot.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/product/akihabot.webp
--------------------------------------------------------------------------------
/public/product/findmaimaidx.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/product/findmaimaidx.webp
--------------------------------------------------------------------------------
/public/product/lxbot.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/product/lxbot.webp
--------------------------------------------------------------------------------
/public/product/maiproberplus.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/product/maiproberplus.webp
--------------------------------------------------------------------------------
/public/product/youmubot.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/product/youmubot.webp
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /user/
3 | Allow: /
--------------------------------------------------------------------------------
/src/App.module.css:
--------------------------------------------------------------------------------
1 | .active {
2 | transition: transform 50ms ease-in-out;
3 |
4 | &:active {
5 | transform: perspective(100px) translate3d(0, 0, -5px);
6 | }
7 | }
8 |
9 | .photoViewerIcon {
10 | color: white;
11 | opacity: 0.75;
12 | transition: opacity 0.2s ease-in-out;
13 | }
14 |
15 | .photoViewerIcon:hover {
16 | opacity: 1;
17 | }
--------------------------------------------------------------------------------
/src/components/Alias/Alias.module.css:
--------------------------------------------------------------------------------
1 | .section {
2 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
3 | }
4 |
5 | .alias {
6 | display: block;
7 | width: 100%;
8 | padding: var(--mantine-spacing-md);
9 | color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
10 |
11 | &:hover {
12 | background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5));
13 | }
14 | }
15 |
16 | .voteButton {
17 | border: 0;
18 | }
--------------------------------------------------------------------------------
/src/components/Alias/AliasButton.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Badge, Center, Flex, Group, Space, Text, ThemeIcon, Tooltip, UnstyledButton, UnstyledButtonProps
3 | } from "@mantine/core";
4 | import { IconCheck, IconChevronRight, IconNorthStar } from "@tabler/icons-react";
5 | import classes from "./Alias.module.css";
6 | import { useEffect, useState } from "react";
7 | import { MaimaiSongProps } from "@/utils/api/song/maimai.ts";
8 | import { ChunithmSongProps } from "@/utils/api/song/chunithm.ts";
9 | import useFixedGame from "@/hooks/useFixedGame.ts";
10 | import useSongListStore from "@/hooks/useSongListStore.ts";
11 | import { useShallow } from "zustand/react/shallow";
12 | import { AliasProps } from "@/types/alias";
13 |
14 | export function AliasButton({ alias, onClick, ...others }: { alias: AliasProps, onClick?: () => void } & UnstyledButtonProps) {
15 | const [song, setSong] = useState();
16 | const [game] = useFixedGame();
17 | const { songList } = useSongListStore(
18 | useShallow((state) => ({ songList: state[game] })),
19 | )
20 |
21 | useEffect(() => {
22 | setSong(songList.find(alias.song.id));
23 | }, [alias.song.id]);
24 |
25 | return (
26 |
27 |
28 |
29 | {alias.song.name || "未知"}
30 |
31 | {game === "maimai" && alias.song.id >= 100000 && (
32 |
33 | 宴
34 |
35 | )}
36 | {game === "chunithm" && alias.song.id >= 8000 && (
37 |
38 | {song && (song as ChunithmSongProps).difficulties[0].kanji}
39 |
40 | )}
41 | {new Date(alias.upload_time).getTime() > new Date().getTime() - 86400000 && (
42 |
43 |
44 |
45 |
46 |
47 |
48 | {alias.approved && (
49 |
50 | )}
51 |
52 | )}
53 | {alias.approved && (
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | )}
62 |
63 |
64 | {alias.alias}
65 |
66 |
67 |
68 | );
69 | }
--------------------------------------------------------------------------------
/src/components/Alias/AliasList.tsx:
--------------------------------------------------------------------------------
1 | import { Box, SimpleGrid } from "@mantine/core";
2 | import { Alias } from "./Alias.tsx";
3 | import { useSetState } from "@mantine/hooks";
4 | import {AliasModal, calculateNewAliasWeight} from "./AliasModal.tsx";
5 | import { useEffect, useState } from "react";
6 | import { useAutoAnimate } from "@formkit/auto-animate/react";
7 | import { AliasProps } from "@/types/alias";
8 |
9 | interface AliasListProps {
10 | aliases: AliasProps[];
11 | onVote: () => void;
12 | onDelete: () => void;
13 | }
14 |
15 | export const AliasList = ({ aliases, onVote, onDelete }: AliasListProps) => {
16 | const [parent] = useAutoAnimate();
17 | const [opened, setOpened] = useState(false);
18 | const [alias, setAlias] = useSetState({} as AliasProps);
19 | const [displayAliases, setDisplayAliases] = useState([]);
20 |
21 | useEffect(() => {
22 | setDisplayAliases(aliases);
23 | }, [aliases]);
24 |
25 | useEffect(() => {
26 | if (alias.alias_id) {
27 | const newDisplayAliases = displayAliases;
28 | const index = displayAliases.findIndex((a) => a.alias_id === alias.alias_id);
29 | if (index !== -1) {
30 | newDisplayAliases[index] = alias;
31 | }
32 | setDisplayAliases(newDisplayAliases);
33 | }
34 | }, [alias]);
35 |
36 | return (
37 |
38 | setOpened(false)} />
39 |
45 | {displayAliases.map((alias) => (
46 | {
50 | if (!alias.uploader) setAlias({
51 | uploader: undefined,
52 | });
53 | setAlias(alias);
54 | setOpened(true);
55 | }}
56 | onVote={(vote) => {
57 | setAlias(calculateNewAliasWeight(alias, vote));
58 | onVote();
59 | }}
60 | onDelete={onDelete}
61 | />
62 | ))}
63 |
64 |
65 | );
66 | }
--------------------------------------------------------------------------------
/src/components/Home/Product.module.css:
--------------------------------------------------------------------------------
1 | .product {
2 | height: 100%;
3 | padding: var(--mantine-spacing-xl);
4 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
5 |
6 | flex-direction: row;
7 | align-items: center;
8 |
9 | row-gap: var(--mantine-spacing-md);
10 | column-gap: var(--mantine-spacing-md);
11 |
12 | @mixin smaller-than $mantine-breakpoint-xs {
13 | flex-direction: column-reverse;
14 | align-items: flex-start;
15 | }
16 | }
--------------------------------------------------------------------------------
/src/components/Home/Product.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, Badge, Button, Card, Group, Stack, Text, Title } from "@mantine/core";
2 | import classes from "./Product.module.css";
3 |
4 | interface ProductProps {
5 | title: string;
6 | tags: string[];
7 | description: string;
8 | image: string;
9 | button: string;
10 | url: string;
11 | }
12 |
13 | export const Product = ({ title, tags, description, image, button, url }: ProductProps) => {
14 | return (
15 |
16 |
17 |
18 |
{title}
19 |
20 | {tags.map((tag) => (
21 | {tag}
22 | ))}
23 |
24 |
25 | {description}
26 |
27 |
28 |
33 |
34 |
35 |
36 | )
37 | }
--------------------------------------------------------------------------------
/src/components/Home/ProductCarousel.tsx:
--------------------------------------------------------------------------------
1 | import { Carousel, Embla } from "@mantine/carousel";
2 | import { useInViewport } from "@mantine/hooks";
3 | import { useEffect, useRef, useState } from "react";
4 | import useShellViewportSize from "@/hooks/useShellViewportSize.ts";
5 | import Autoplay from "embla-carousel-autoplay";
6 |
7 | import { Product } from "@/components/Home/Product.tsx";
8 | import products from '@/data/products.json';
9 |
10 | const isTouchScreen = () => {
11 | return window.matchMedia('(pointer: coarse)').matches;
12 | };
13 |
14 | export const ProductCarousel = () => {
15 | const autoplay = useRef(Autoplay({
16 | delay: 2000,
17 | playOnInit: false,
18 | }));
19 | const { ref, inViewport } = useInViewport();
20 | const { width } = useShellViewportSize();
21 | const [containerWidth, setContainerWidth] = useState(width);
22 | const [embla, setEmbla] = useState(null);
23 |
24 | useEffect(() => {
25 | if (width < 576) {
26 | setContainerWidth(width - 32);
27 | } else {
28 | setContainerWidth(width - 64);
29 | }
30 | }, [width]);
31 |
32 | useEffect(() => {
33 | if (embla) {
34 | embla.reInit();
35 | }
36 | }, [embla, containerWidth]);
37 |
38 | useEffect(() => {
39 | if (inViewport) {
40 | autoplay.current.play();
41 | } else {
42 | autoplay.current.stop();
43 | }
44 | }, [inViewport]);
45 |
46 | return (
47 |
60 | {products.map((product, index) => (
61 |
62 |
63 |
64 | ))}
65 |
66 | );
67 | }
--------------------------------------------------------------------------------
/src/components/LoginAlert.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, Button, Group, Text, Transition } from "@mantine/core";
2 | import { IconAlertCircle } from "@tabler/icons-react";
3 | import React, { useEffect, useState } from "react";
4 | import { useNavigate } from "react-router-dom";
5 |
6 | interface LoginAlertProps extends React.ComponentPropsWithoutRef {
7 | content: string;
8 | props?: never;
9 | }
10 |
11 | export const LoginAlert = ({ content, ...props }: LoginAlertProps) => {
12 | const [opened, setOpened] = useState(false);
13 | const isLoggedOut = !localStorage.getItem("token");
14 | const navigate = useNavigate();
15 |
16 | useEffect(() => {
17 | if (isLoggedOut) {
18 | setOpened(true);
19 | }
20 | }, [isLoggedOut]);
21 |
22 | return (
23 |
24 | {(styles) => (
25 | }
28 | title="登录提示"
29 | withCloseButton
30 | onClose={() => setOpened(false)}
31 | style={styles}
32 | {...props}
33 | >
34 |
35 | {content}
36 |
37 |
38 |
41 |
44 |
45 |
46 | )}
47 |
48 | )
49 | }
--------------------------------------------------------------------------------
/src/components/Marquee.tsx:
--------------------------------------------------------------------------------
1 | import { Children, ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
2 | import { Group } from "@mantine/core";
3 | import { useHoverDirty } from "react-use";
4 | import { useAnimationFrame } from "motion/react"
5 |
6 | interface MarqueeProps {
7 | speed?: number;
8 | startDelay?: number; // 开始滚动的延迟
9 | intervalDelay?: number; // 滚动间隔
10 | pauseOnHover?: boolean; // 鼠标悬停暂停滚动
11 | children: ReactNode;
12 | props?: any;
13 | }
14 |
15 | export const Marquee = ({ speed = 0.5, startDelay = 1000, intervalDelay = 1000, pauseOnHover = true, children, ...props }: MarqueeProps) => {
16 | const [isScrolling, setIsScrolling] = useState(false); // 是否需要滚动
17 | const [isPaused, setIsPaused] = useState(false); // 是否暂停滚动
18 | const [translateX, setTranslateX] = useState(0);
19 |
20 | const directionRef = useRef(1);
21 | const delayUntilRef = useRef(0);
22 |
23 | const ref = useRef(null);
24 | const isHovering = useHoverDirty(ref); // 是否鼠标悬停
25 |
26 | useAnimationFrame((time) => {
27 | if (!isScrolling || isPaused) return;
28 |
29 | // 若处于延迟期,不滚动
30 | if (delayUntilRef.current && time < delayUntilRef.current) return;
31 |
32 | if (!ref.current) return;
33 |
34 | setTranslateX((prev) => {
35 | const newTranslateX = prev - directionRef.current * speed;
36 | const maxTranslateX = ref.current!.scrollWidth - ref.current!.clientWidth;
37 |
38 | if (newTranslateX <= -maxTranslateX - speed || newTranslateX >= speed) {
39 | directionRef.current = -directionRef.current; // 反转方向
40 | delayUntilRef.current = time + intervalDelay; // 设置延迟截止时间
41 | return prev; // 暂停这一帧,等下次再移动
42 | }
43 |
44 | return newTranslateX;
45 | });
46 | });
47 |
48 | useEffect(() => {
49 | if (!isScrolling) {
50 | setTranslateX(0);
51 | }
52 | }, [isScrolling]);
53 |
54 | useLayoutEffect(() => {
55 | if (pauseOnHover) {
56 | setIsPaused(isHovering);
57 | }
58 |
59 | if (ref.current && ref.current.scrollWidth > ref.current.clientWidth) {
60 | setTimeout(() => {
61 | setIsScrolling(true);
62 | }, startDelay);
63 | } else {
64 | setIsScrolling(false);
65 | }
66 | });
67 |
68 | return
69 | {Children.map(children, (child) => (
70 | {child}
75 | ))}
76 |
77 | }
--------------------------------------------------------------------------------
/src/components/Page/Page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { PageTabs } from "./PageTabs.tsx";
3 | import { PageHeader } from "./PageHeader.tsx";
4 | import { PageRawContent } from "./PageRawContent.tsx";
5 | import { NAVBAR_BREAKPOINT } from "@/App.tsx";
6 | import { Helmet } from "react-helmet";
7 |
8 | export interface PageProps {
9 | meta: {
10 | title: string;
11 | description: string;
12 | };
13 | tabs?: {
14 | id: string;
15 | name: string;
16 | children: React.ReactNode;
17 | }[];
18 | children?: React.ReactNode;
19 | }
20 |
21 | export const Page = (props: PageProps) => {
22 | return (
23 |
26 |
30 | {props.meta.title && {props.meta.title}}
31 | {props.meta.description && }
32 |
33 |
34 |
35 |
36 | {props.tabs && (
37 |
38 | )}
39 |
40 | {props.children && (
41 |
42 | )}
43 |
44 | );
45 | }
--------------------------------------------------------------------------------
/src/components/Page/PageHeader.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
4 |
5 | @mixin smaller-than $mantine-breakpoint-md {
6 | text-align: center;
7 | }
8 | }
9 |
10 | .header {
11 | padding: var(--mantine-spacing-xl) 16px var(--mantine-spacing-xl);
12 | max-width: var(--page-max-width);
13 | margin-left: auto;
14 | margin-right: auto;
15 |
16 | @mixin smaller-than $mantine-breakpoint-xs {
17 | padding: var(--mantine-spacing-lg) 16px var(--mantine-spacing-lg);
18 | }
19 | }
20 |
21 |
22 | .title {
23 | font-size: 32px;
24 | margin-bottom: 5px;
25 | font-weight: 900;
26 | color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
27 |
28 | @mixin smaller-than $mantine-breakpoint-xs {
29 | font-size: 28px;
30 | }
31 | }
32 |
33 | .description {
34 | font-size: var(--mantine-font-size-md);
35 | color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-1));
36 |
37 | @mixin smaller-than $mantine-breakpoint-xs {
38 | font-size: var(--mantine-font-size-sm);
39 | }
40 | }
--------------------------------------------------------------------------------
/src/components/Page/PageHeader.tsx:
--------------------------------------------------------------------------------
1 | import { PageProps } from "./Page.tsx";
2 | import { Box, Text, Title } from "@mantine/core";
3 | import classes from "./PageHeader.module.css";
4 |
5 | export const PageHeader = ({ meta }: PageProps) => {
6 | return (
7 |
8 |
9 | {meta.title}
10 | {meta.description}
11 |
12 |
13 | )
14 | }
--------------------------------------------------------------------------------
/src/components/Page/PageRawContent.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | max-width: var(--page-max-width);
3 | margin-left: auto;
4 | margin-right: auto;
5 | padding: 16px;
6 | }
--------------------------------------------------------------------------------
/src/components/Page/PageRawContent.tsx:
--------------------------------------------------------------------------------
1 | import { PageProps } from "./Page.tsx";
2 | import classes from "./PageRawContent.module.css";
3 |
4 | export const PageRawContent = (props: PageProps) => {
5 | return (
6 |
7 | {props.children}
8 |
9 | )
10 | }
--------------------------------------------------------------------------------
/src/components/Page/PageTabs.module.css:
--------------------------------------------------------------------------------
1 | .tabsWrapper {
2 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
3 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
4 | }
5 |
6 | .tabsList {
7 | max-width: var(--page-max-width);
8 | margin-left: auto;
9 | margin-right: auto;
10 | margin-bottom: -1px;
11 | padding: 0 16px;
12 |
13 | @mixin smaller-than $mantine-breakpoint-xs {
14 | max-width: 100%;
15 | padding-right: 0;
16 | }
17 |
18 | &::before {
19 | border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
20 | }
21 | }
22 |
23 | .tab {
24 | font-size: var(--mantine-font-size-md);
25 | font-weight: 500;
26 | height: 46px;
27 | padding-left: var(--mantine-spacing-lg);
28 | padding-right: var(--mantine-spacing-lg);
29 | background-color: transparent;
30 |
31 | @mixin smaller-than $mantine-breakpoint-xs {
32 | font-size: var(--mantine-font-size-sm);
33 | padding-left: var(--mantine-spacing-md);
34 | padding-right: var(--mantine-spacing-md);
35 | }
36 |
37 | &[data-active] {
38 | background-color: var(--mantine-color-body);
39 |
40 | @mixin light {
41 | color: var(--mantine-color-black);
42 | border-color: var(--mantine-color-gray-2);
43 | border-bottom-color: transparent;
44 |
45 | &::before,
46 | &::after {
47 | background-color: var(--mantine-color-gray-2);
48 | }
49 | }
50 |
51 | @mixin dark {
52 | color: var(--mantine-color-white);
53 | border-color: var(--mantine-color-dark-6);
54 | border-bottom-color: transparent;
55 |
56 | &::before,
57 | &::after {
58 | background-color: var(--mantine-color-dark-6);
59 | }
60 | }
61 | }
62 | }
63 |
64 | .tabContent {
65 | max-width: var(--page-max-width);
66 | margin-left: auto;
67 | margin-right: auto;
68 | padding: 16px;
69 | }
--------------------------------------------------------------------------------
/src/components/Page/PageTabs.tsx:
--------------------------------------------------------------------------------
1 | import { PageProps } from "./Page.tsx";
2 | import { Tabs } from "@mantine/core";
3 | import { useState } from "react";
4 | import classes from "./PageTabs.module.css";
5 | import { useSearchParams } from "react-router-dom";
6 |
7 | export const PageTabs = (props: PageProps) => {
8 | const [searchParams, setSearchParams] = useSearchParams();
9 | const [activeTab, setActiveTab] = useState(searchParams.get('tab') || props.tabs?.[0].id);
10 |
11 | return (
12 | {
19 | setSearchParams({ tab: tab! });
20 | setActiveTab(tab!)
21 | }}
22 | >
23 |
24 |
25 | {props.tabs?.map((tab) => (
26 |
27 | {tab.name}
28 |
29 | ))}
30 |
31 |
32 |
33 | {props.tabs?.map((tab) => (
34 |
35 |
36 | {tab.children}
37 |
38 |
39 | ))}
40 |
41 | )
42 | }
--------------------------------------------------------------------------------
/src/components/Profile/PlayerPanel/NotFoundAlert.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from "react-router-dom";
2 | import { Alert, Button, Group, Text} from "@mantine/core";
3 | import Icon from "@mdi/react";
4 | import { mdiAlertCircleOutline } from "@mdi/js";
5 |
6 | export const NotFoundAlert = () => {
7 | const navigate = useNavigate();
8 |
9 | return (
10 | } title="没有获取到游戏数据" color="red">
11 |
12 | 请检查你的查分器账号是否已经绑定游戏账号。
13 |
14 |
15 |
18 |
19 |
20 | )
21 | }
--------------------------------------------------------------------------------
/src/components/Profile/PlayerPanel/PlayerModal.module.css:
--------------------------------------------------------------------------------
1 | @import "../../Scores/ScoreModalMenu.module.css";
2 |
3 | .subParameters {
4 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
5 | padding: 6px 12px;
6 | border-radius: var(--mantine-radius-md);
7 | }
--------------------------------------------------------------------------------
/src/components/Profile/PlayerPanel/PlayerPanel.module.css:
--------------------------------------------------------------------------------
1 | .section {
2 | padding: var(--mantine-spacing-md);
3 | }
4 |
5 | .playerButton {
6 | display: block;
7 | width: 100%;
8 | padding: var(--mantine-spacing-md);
9 | color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
10 |
11 | &:hover {
12 | background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
13 | }
14 | }
--------------------------------------------------------------------------------
/src/components/Profile/PlayerPanel/PlayerPanel.tsx:
--------------------------------------------------------------------------------
1 | import { Divider, Group, ScrollArea, Text, UnstyledButton, UnstyledButtonProps } from "@mantine/core";
2 | import { useState } from "react";
3 | import { useViewportSize } from "@mantine/hooks";
4 | import classes from "./PlayerPanel.module.css"
5 | import { ChunithmPlayerProps, MaimaiPlayerProps } from "@/types/player";
6 | import { PlayerModal } from "./PlayerModal.tsx";
7 | import { MaimaiPlayerContent } from "./maimai/PlayerContent.tsx";
8 | import { ChunithmPlayerContent } from "./chunithm/PlayerContent.tsx";
9 | import { isChunithmPlayerProps, isMaimaiPlayerProps } from "@/utils/api/player.ts";
10 | import useGame from "@/hooks/useGame.ts";
11 |
12 | const examplePlayer = {
13 | maimai: {
14 | name: "maimai",
15 | rating: 0,
16 | friend_code: 888888888888888,
17 | trophy: {
18 | "name": "欢迎来到“舞萌DX”!",
19 | "color": "Normal"
20 | },
21 | course_rank: 0,
22 | class_rank: 0,
23 | star: 0,
24 | icon: {
25 | id: 1,
26 | name: ""
27 | },
28 | upload_time: "2024-01-01T08:00:00Z"
29 | },
30 | chunithm: {
31 | name: "CHUNITHM",
32 | level: 1,
33 | rating: 0,
34 | friend_code: 888888888888888,
35 | class_emblem: {
36 | base: 0,
37 | medal: 0
38 | },
39 | reborn_count: 0,
40 | over_power: 0,
41 | over_power_progress: 0,
42 | currency: 0,
43 | total_currency: 0,
44 | trophy: {
45 | name: "NEW COMER",
46 | color: "normal"
47 | },
48 | character: {
49 | id: 0,
50 | name: ""
51 | },
52 | upload_time: "2024-01-01T08:00:00Z"
53 | },
54 | }
55 |
56 | interface PlayerButtonProps {
57 | player: MaimaiPlayerProps | ChunithmPlayerProps;
58 | onClick?: () => void;
59 | }
60 |
61 | const PlayerButton = ({ player, onClick, ...others }: PlayerButtonProps & UnstyledButtonProps) => {
62 | return (
63 |
64 | {isMaimaiPlayerProps(player) && }
65 | {isChunithmPlayerProps(player) && }
66 |
67 | );
68 | }
69 |
70 | export const PlayerPanel = ({ player }: { player?: MaimaiPlayerProps | ChunithmPlayerProps }) => {
71 | const { width } = useViewportSize();
72 | const [game] = useGame();
73 | const [opened, setOpened] = useState(false);
74 |
75 | if (!player) player = examplePlayer[game];
76 |
77 | return (
78 | <>
79 | setOpened(false)} />
80 |
81 | setOpened(true)} />
82 |
83 |
84 |
85 |
86 | 好友码
87 | {player.friend_code}
88 |
89 |
90 | 上次同步时间
91 | {(new Date(Date.parse(player.upload_time))).toLocaleString()}
92 |
93 |
94 | >
95 | )
96 | }
--------------------------------------------------------------------------------
/src/components/Profile/PlayerPanel/Skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Grid, rem, Skeleton } from "@mantine/core";
2 |
3 | export const PlayerPanelSkeleton = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
--------------------------------------------------------------------------------
/src/components/Profile/PlayerPanel/chunithm/PlayerModal.tsx:
--------------------------------------------------------------------------------
1 | import { Container, Grid, Paper, Text } from "@mantine/core";
2 | import { ChunithmPlayerProps } from "@/types/player";
3 | import classes from "../PlayerModal.module.css";
4 | import { Marquee } from "@/components/Marquee.tsx";
5 |
6 | export const ChunithmPlayerModalContent = ({ player }: { player: ChunithmPlayerProps }) => {
7 | return (
8 |
9 |
10 |
11 | {player.name_plate && (
12 |
13 | 名牌版
14 |
15 |
16 |
17 |
18 | )}
19 |
20 |
21 | {player.map_icon && (
22 |
23 | 地图头像
24 |
25 |
26 |
27 |
28 | )}
29 |
30 |
31 |
32 | 上次同步时间
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Profile/PlayerPanel/chunithm/RatingTrend.tsx:
--------------------------------------------------------------------------------
1 | import { Card, Flex, Text } from "@mantine/core";
2 | import { IconDatabaseOff } from "@tabler/icons-react";
3 | import { Area, CartesianGrid, ComposedChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
4 |
5 | export interface ChunithmRatingTrendProps {
6 | rating: number;
7 | bests_rating: number;
8 | selections_rating: number;
9 | recents_rating: number;
10 | date: string | number;
11 | }
12 |
13 | const RatingTrendChart = ({ trend }: { trend: ChunithmRatingTrendProps[] }) => {
14 | trend = trend.map((item) => {
15 | return {
16 | ...item,
17 | date: new Date(item.date).getTime(),
18 | }
19 | })
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | new Date(value).toLocaleDateString('zh-CN', {
31 | month: "numeric",
32 | day: "numeric",
33 | })} fontSize={14} />
34 | {
35 | dataMin -= 0.01;
36 | dataMax += 0.01;
37 | return [Math.round(dataMin*100)/100, Math.round(dataMax*100)/100];
38 | }} fontSize={14} />
39 |
40 | {
41 | if (!props.active || !props.payload || props.payload.length < 1) return null;
42 | const payload = props.payload[0].payload;
43 | return (
44 |
45 | {new Date(payload.date).toLocaleDateString()}
46 | Rating: {Math.round(payload.rating*100)/100}
47 | Best 30: {Math.round(payload.bests_rating*100)/100}
48 | Selection 10: {Math.round(payload.selections_rating*100)/100}
49 | Recent 10 (MAX): {Math.round(payload.recents_rating*100)/100}
50 |
51 | )
52 | }} />
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export const ChunithmRatingTrend = ({ trend }: { trend: ChunithmRatingTrendProps[] | null }) => {
60 | if (!trend || trend.length < 2) {
61 | return (
62 |
63 |
64 | 历史记录不足,无法生成图表
65 |
66 | )
67 | }
68 |
69 | return <>
70 |
71 | ※ Recent 10 均为 Best #1 曲目,最终结果为理论不推分最高 Rating。
72 | ※ 该数据由历史同步成绩推出,而非玩家的历史 Rating,结果仅供参考。
73 | >
74 | }
--------------------------------------------------------------------------------
/src/components/Profile/PlayerPanel/maimai/PlayerContent.tsx:
--------------------------------------------------------------------------------
1 | import { MaimaiPlayerProps } from "@/types/player";
2 | import { Avatar, Badge, Divider, Flex, Group, Image, rem, Text, useComputedColorScheme } from "@mantine/core";
3 | import { IconPhotoOff } from "@tabler/icons-react";
4 | import { getDeluxeRatingGradient, getTrophyColor } from "@/utils/color.ts";
5 | import { ASSET_URL } from "@/main.tsx";
6 | import { Marquee } from "@/components/Marquee.tsx";
7 |
8 | export const MaimaiPlayerContent = ({ player }: { player: MaimaiPlayerProps }) => {
9 | const computedColorScheme = useComputedColorScheme('light');
10 |
11 | return (
12 |
13 | ({
14 | root: {
15 | backgroundColor: computedColorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[1],
16 | }
17 | })}>
18 |
19 |
20 |
21 |
22 | {player.trophy && (
23 |
25 |
28 | {player.trophy.name}
29 |
30 |
31 | } />
32 | )}
33 | DX Rating: {player.rating}
36 |
37 |
38 |
39 | {player.name}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | ×{player.star}
49 |
50 |
51 |
52 |
53 |
54 | )
55 | }
--------------------------------------------------------------------------------
/src/components/Profile/PlayerPanel/maimai/PlayerModal.tsx:
--------------------------------------------------------------------------------
1 | import { Container, Grid, Paper, Text } from "@mantine/core";
2 | import { MaimaiPlayerProps } from "@/types/player";
3 | import classes from "../PlayerModal.module.css";
4 | import { Marquee } from "@/components/Marquee.tsx";
5 |
6 | export const MaimaiPlayerModalContent = ({ player }: { player: MaimaiPlayerProps }) => {
7 | return (
8 |
9 |
10 |
11 | {player.name_plate && (
12 |
13 | 姓名框
14 |
15 |
16 |
17 |
18 | )}
19 |
20 |
21 | {player.frame && (
22 |
23 | 背景板
24 |
25 |
26 |
27 |
28 | )}
29 |
30 |
31 |
32 | 上次同步时间
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Profile/PlayerPanel/maimai/RatingTrend.tsx:
--------------------------------------------------------------------------------
1 | import { Card, Flex, Group, Text } from "@mantine/core";
2 | import { IconDatabaseOff } from "@tabler/icons-react";
3 | import { Area, CartesianGrid, ComposedChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
4 |
5 | export interface MaimaiRatingTrendProps {
6 | total: number;
7 | standard_total: number;
8 | dx_total: number;
9 | date: string | number;
10 | }
11 |
12 | const RatingTrendChart = ({ trend }: { trend: MaimaiRatingTrendProps[] }) => {
13 | trend = trend.map((item) => {
14 | return {
15 | ...item,
16 | date: new Date(item.date).getTime(),
17 | }
18 | })
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | new Date(value).toLocaleDateString('zh-CN', {
30 | month: "numeric",
31 | day: "numeric",
32 | })} fontSize={14} />
33 | {
34 | return [Math.floor(dataMin), Math.floor(dataMax)];
35 | }} fontSize={14} />
36 |
37 | {
38 | if (!props.active || !props.payload || props.payload.length < 1) return null;
39 | const payload = props.payload[0].payload;
40 | return (
41 |
42 | {new Date(payload.date).toLocaleDateString()}
43 | DX Rating: {payload.total}
44 |
45 | B35: {payload.standard_total}
46 | B15: {payload.dx_total}
47 |
48 |
49 | )
50 | }} />
51 |
52 |
53 |
54 | )
55 | }
56 |
57 | export const MaimaiRatingTrend = ({ trend }: { trend: MaimaiRatingTrendProps[] | null }) => {
58 | if (!trend || trend.length < 2) {
59 | return (
60 |
61 |
62 | 历史记录不足,无法生成图表
63 |
64 | )
65 | }
66 | return <>
67 |
68 | ※ 该数据由历史同步成绩推出,而非玩家的历史 DX Rating,结果仅供参考。
69 | >
70 | }
--------------------------------------------------------------------------------
/src/components/Profile/PlayerSection.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, Overlay, Stack, Text, useComputedColorScheme } from "@mantine/core";
2 | import { PlayerPanelSkeleton } from "./PlayerPanel/Skeleton.tsx";
3 | import { useNavigate } from "react-router-dom";
4 | import classes from "./Profile.module.css";
5 | import { usePlayer } from "@/hooks/swr/usePlayer.ts";
6 | import { PlayerPanel } from "./PlayerPanel/PlayerPanel.tsx";
7 | import useGame from "@/hooks/useGame.ts";
8 |
9 | export const PlayerSection = () => {
10 | const [game] = useGame();
11 | const { player, isLoading } = usePlayer(game);
12 | const computedColorScheme = useComputedColorScheme('light');
13 | const navigate = useNavigate();
14 |
15 | return (
16 |
17 | {isLoading ? (
18 |
19 | ) : (
20 |
21 | {!player && (
22 |
25 |
26 | 尚未同步游戏数据
27 |
30 |
31 |
32 | )}
33 |
34 |
35 | )}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/Profile/Profile.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
3 | }
4 |
5 | .section {
6 | padding: var(--mantine-spacing-md);
7 | }
8 |
9 | .tab {
10 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
11 | color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0));
12 | border: 0;
13 | border-top: 1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-6));
14 | padding: 8px var(--mantine-spacing-md);
15 | cursor: pointer;
16 | font-size: var(--mantine-font-size-sm);
17 | flex: 1;
18 |
19 | @mixin hover {
20 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
21 | }
22 |
23 | &[data-active] {
24 | background-color: var(--mantine-primary-color-light);
25 | color: var(--mantine-primary-color-light-color);
26 |
27 | @mixin hover {
28 | background-color: var(--mantine-primary-color-light-hover);
29 | }
30 | }
31 | }
32 |
33 | .list {
34 | display: flex;
35 | }
--------------------------------------------------------------------------------
/src/components/Profile/UserBindSection.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, Group, Switch, Text, TextInput } from "@mantine/core";
2 | import { useForm } from "@mantine/form";
3 | import { updateUserBind } from "@/utils/api/user.ts";
4 | import Icon from "@mdi/react";
5 | import { mdiEye, mdiEyeOff } from "@mdi/js";
6 | import { useDisclosure } from "@mantine/hooks";
7 | import classes from "./Profile.module.css";
8 | import { openAlertModal, openRetryModal } from "../../utils/modal.tsx";
9 | import { useUser } from "@/hooks/swr/useUser.ts";
10 |
11 | export const UserBindSection = () => {
12 | const { user, mutate } = useUser();
13 | const [visible, visibleHandler] = useDisclosure(false)
14 |
15 | const form = useForm({
16 | initialValues: {
17 | qq: '',
18 | },
19 |
20 | validate: {
21 | qq: (value: string) => /^\d{5,11}$/.test(value) ? null : "QQ 号格式不正确",
22 | },
23 |
24 | transformValues: (values) => ({
25 | qq: parseInt(values.qq),
26 | })
27 | });
28 |
29 | const updateUserBindHandler = async () => {
30 | try {
31 | const res = await updateUserBind(form.getTransformedValues())
32 | const data = await res.json()
33 | if (!data.success) {
34 | throw new Error(data.message)
35 | }
36 | openAlertModal("绑定成功", "第三方开发者将可以通过绑定信息获取你的游戏数据。");
37 | mutate({ ...user, bind: { ...(user?.bind || {}), qq: parseInt(form.values.qq)} } as any, false);
38 | } catch (error) {
39 | openRetryModal("绑定失败", `${error}`, updateUserBindHandler);
40 | } finally {
41 | form.reset();
42 | }
43 | }
44 |
45 | return (
46 |
47 |
48 |
49 |
50 | 第三方账号绑定
51 |
52 |
53 | 绑定第三方账号,第三方开发者将可以通过绑定信息获取你的游戏数据
54 |
55 |
56 | }
61 | offLabel={}
62 | />
63 |
64 |
77 |
78 | )
79 | }
--------------------------------------------------------------------------------
/src/components/RadioCardGroup.module.css:
--------------------------------------------------------------------------------
1 | .root {
2 | position: relative;
3 | padding: var(--mantine-spacing-md);
4 | transition: border-color 150ms ease;
5 |
6 | &[data-checked] {
7 | border-color: var(--mantine-primary-color-filled);
8 | }
9 |
10 | @mixin hover {
11 | @mixin light {
12 | background-color: var(--mantine-color-gray-0);
13 | }
14 |
15 | @mixin dark {
16 | background-color: var(--mantine-color-dark-6);
17 | }
18 | }
19 | }
20 |
21 | .label {
22 | font-weight: bold;
23 | font-size: var(--mantine-font-size-md);
24 | line-height: 1.3;
25 | color: var(--mantine-color-bright);
26 | }
27 |
28 | .description {
29 | margin-top: 8px;
30 | color: var(--mantine-color-dimmed);
31 | font-size: var(--mantine-font-size-xs);
32 | }
--------------------------------------------------------------------------------
/src/components/RadioCardGroup.tsx:
--------------------------------------------------------------------------------
1 | import { Radio, Group, Stack, Text } from '@mantine/core';
2 | import classes from './RadioCardGroup.module.css';
3 |
4 | interface RadioCardGroupProps {
5 | data: { name: string; description: string; value: string }[];
6 | value?: string;
7 | onChange?: (value: string) => void;
8 | }
9 |
10 | export const RadioCardGroup = ({ data, value, onChange }: RadioCardGroupProps) => {
11 | const cards = data.map((item) => (
12 |
13 |
14 |
15 |
16 | {item.name}
17 | {item.description}
18 |
19 |
20 |
21 | ));
22 |
23 | return
27 |
28 | {cards}
29 |
30 |
31 | }
--------------------------------------------------------------------------------
/src/components/RouterTransition.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useLocation } from "react-router"
3 | import { NavigationProgress, nprogress } from '@mantine/nprogress';
4 |
5 | export default function RouterTransition() {
6 | const location = useLocation();
7 |
8 | useEffect(() => {
9 | return () => {
10 | nprogress.start();
11 | nprogress.complete();
12 | };
13 | }, [location.pathname])
14 |
15 | return ;
16 | }
--------------------------------------------------------------------------------
/src/components/Scores/ChartComment.module.css:
--------------------------------------------------------------------------------
1 | .actionIcon {
2 | color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
3 |
4 | &:hover {
5 | color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
6 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
7 | }
8 | }
9 |
10 | .textarea {
11 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
12 | }
--------------------------------------------------------------------------------
/src/components/Scores/RatingSegments.module.css:
--------------------------------------------------------------------------------
1 | .subParameters {
2 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
3 | padding: 6px 12px;
4 | border-radius: var(--mantine-radius-md);
5 | }
6 |
7 | .statCount {
8 | line-height: 1.3;
9 | }
10 |
11 | .diff {
12 | display: flex;
13 | align-items: center;
14 | }
15 |
16 | .icon {
17 | color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
18 | }
19 |
20 | .ratingNumberTotal {
21 | font-size: var(--mantine-h2-font-size);
22 | font-family: 'Roboto Mono', 'Courier New', monospace;
23 | font-weight: 700;
24 | line-height: var(--mantine-h2-line-height);
25 | }
26 |
27 | .ratingNumberSubtotal {
28 | color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
29 | font-size: var(--mantine-h4-font-size);
30 | font-family: 'Roboto Mono', 'Courier New', monospace;
31 | font-weight: 700;
32 | line-height: var(--mantine-h4-line-height);
33 | }
--------------------------------------------------------------------------------
/src/components/Scores/ScoreModal.module.css:
--------------------------------------------------------------------------------
1 | @import "./ScoreModalMenu.module.css";
2 |
3 | .subParameters {
4 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
5 | padding: 6px 12px;
6 | border-radius: var(--mantine-radius-md);
7 | }
8 |
9 | .subParametersButton {
10 | cursor: pointer;
11 | }
12 |
13 | .subParametersButton:hover {
14 | background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
15 | }
16 |
17 | .worldsEndText {
18 | background: conic-gradient(
19 | from 240deg,
20 | rgb(2, 224, 1),
21 | rgb(213, 226, 17),
22 | rgb(226, 121, 87),
23 | rgb(212, 1, 150),
24 | rgb(141, 12, 202),
25 | rgb(77,120,107),
26 | rgb(2, 224, 1)
27 | );
28 | -webkit-text-fill-color: transparent;
29 | -webkit-background-clip: text;
30 | background-clip: text;
31 | }
--------------------------------------------------------------------------------
/src/components/Scores/ScoreModalMenu.module.css:
--------------------------------------------------------------------------------
1 | .actionIcon {
2 | color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
3 |
4 | &:hover {
5 | color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
6 | background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
7 | }
8 | }
--------------------------------------------------------------------------------
/src/components/Scores/Scores.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
3 | }
4 |
5 | .scoreCard {
6 | cursor: pointer;
7 | transition: transform 200ms ease;
8 |
9 | &:hover {
10 | transform: scale(1.03);
11 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
12 | box-shadow: var(--mantine-shadow-md);
13 | border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
14 | border-radius: var(--mantine-radius-md);
15 | z-index: 1;
16 | }
17 | }
18 |
19 | .scoreWorldsEnd {
20 | --angle: 0deg;
21 | padding: 2px !important;
22 | }
23 |
24 | .scoreWorldsEnd::before {
25 | content: '';
26 | position: absolute;
27 | top: 0;
28 | left: 0;
29 | right: 0;
30 | bottom: 0;
31 | border-radius: var(--mantine-radius-md);
32 | padding: 2px;
33 | background: conic-gradient(from var(--angle), red, yellow, lime, aqua, blue, magenta, red);
34 | -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
35 | -webkit-mask-composite: xor;
36 | mask-composite: exclude;
37 | animation: 10s rotate linear infinite;
38 | }
39 |
40 | @keyframes rotate {
41 | to {
42 | --angle: 360deg;
43 | }
44 | }
45 |
46 | @property --angle {
47 | syntax: '';
48 | initial-value: 0deg;
49 | inherits: false;
50 | }
--------------------------------------------------------------------------------
/src/components/Scores/chunithm/Chart.tsx:
--------------------------------------------------------------------------------
1 | import { ChunithmDifficultyProps, ChunithmNotesProps } from "@/utils/api/song/chunithm.ts";
2 | import { Box, Group, keys, Space, Table, Text } from "@mantine/core";
3 | import useSongListStore from "@/hooks/useSongListStore.ts";
4 | import { useShallow } from "zustand/react/shallow";
5 |
6 | const ChartNotes = ({ notes }: { notes: ChunithmNotesProps }) => {
7 | if (!notes) return;
8 |
9 | return
10 |
11 |
12 | {keys(notes).map((key) => {
13 | return
14 | {key.toLocaleUpperCase()}
15 | {notes[key]}
16 | ;
17 | })}
18 |
19 |
20 | ;
21 | }
22 |
23 | export const ChunithmChart = ({ difficulty }: { difficulty: ChunithmDifficultyProps }) => {
24 | const { versions } = useSongListStore(
25 | useShallow((state) => ({ versions: state.chunithm.versions })),
26 | )
27 |
28 | if (!difficulty) return;
29 |
30 | return <>
31 |
32 | {difficulty.note_designer && (
33 |
34 | 谱师
35 | {difficulty.note_designer}
36 |
37 | )}
38 |
39 | 版本
40 |
41 | {versions.slice().reverse().find((version) => difficulty.version >= version.version)?.title || "未知"}
42 |
43 |
44 |
45 |
46 | 物量
47 |
48 | >;
49 | }
--------------------------------------------------------------------------------
/src/components/Scores/chunithm/Score.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Card, Flex, Group, NumberFormatter, Rating, rem, Text } from "@mantine/core";
2 | import { getScoreCardBackgroundColor, getScoreSecondaryColor, getTransparentColor } from "@/utils/color.ts";
3 | import { getDifficulty, ChunithmSongProps, ChunithmDifficultyProps } from "@/utils/api/song/chunithm.ts";
4 | import { useEffect, useState } from "react";
5 | import { ChunithmScoreProps } from "@/types/score";
6 |
7 | interface ScoreContentProps {
8 | score: ChunithmScoreProps;
9 | song?: ChunithmSongProps;
10 | }
11 |
12 | export const ChunithmScoreContent = ({ score, song }: ScoreContentProps) => {
13 | const [difficulty, setDifficulty] = useState(null);
14 | const [level, setLevel] = useState(score.level);
15 |
16 | const rating = score.id < 8000 ? `${Math.floor(score.rating * 100) / 100}` : "-";
17 | const levelIndex = score.id < 8000 ? score.level_index : 5;
18 |
19 | useEffect(() => {
20 | if (!song) return;
21 | const difficulty = getDifficulty(song, score.level_index);
22 | if (!difficulty) return;
23 | setDifficulty(difficulty);
24 | if (score.id >= 8000) {
25 | setLevel(difficulty.kanji);
26 | } else {
27 | setLevel(difficulty.level_value.toFixed(1));
28 | }
29 | }, [song]);
30 |
31 | return <>
32 |
35 | {score.song_name}
36 | {score.id >= 8000 && difficulty &&
37 |
38 | }
39 |
40 |
43 |
44 | {score.score != -1 ? (
45 |
46 |
47 |
48 |
49 |
50 | Rating: {rating}
51 |
52 |
53 | ) : (
54 |
55 |
56 | 未游玩
57 |
58 |
59 | 或未上传至查分器
60 |
61 |
62 | )}
63 |
64 |
67 | {level}
68 |
69 |
70 |
71 |
72 | >
73 | }
--------------------------------------------------------------------------------
/src/components/Scores/maimai/DeluxeRatingCalculator.module.css:
--------------------------------------------------------------------------------
1 | .changeLabel {
2 | position: relative;
3 | }
4 |
5 | .changeLabel::after {
6 | content: attr(data-label);
7 | position: absolute;
8 | left: 0.5rem;
9 | bottom: -0.5rem;
10 | padding: 0.1rem 0.25rem;
11 | border-radius: 0.1rem;
12 | font-size: 0.75rem;
13 | line-height: 1;
14 | font-weight: 500;
15 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
16 | }
--------------------------------------------------------------------------------
/src/components/Scores/maimai/Score.tsx:
--------------------------------------------------------------------------------
1 | import { Badge, Box, Card, Flex, Group, rem, Text } from "@mantine/core";
2 | import {
3 | getScoreCardBackgroundColor, getScoreSecondaryColor, getTransparentColor,
4 | } from "@/utils/color.ts";
5 | import { getDifficulty, MaimaiSongProps } from "@/utils/api/song/maimai.ts";
6 | import { useEffect, useState } from "react";
7 | import { MaimaiScoreProps } from "@/types/score";
8 |
9 | interface ScoreContentProps {
10 | score: MaimaiScoreProps;
11 | song?: MaimaiSongProps;
12 | }
13 |
14 | export const MaimaiScoreContent = ({ score, song }: ScoreContentProps) => {
15 | const [isBuddy, setIsBuddy] = useState(false);
16 | const [level, setLevel] = useState(score.level);
17 |
18 | const deluxeRating = score.type !== "utage" ? `${parseInt(String(score.dx_rating))}` : "-";
19 | const levelIndex = score.type !== "utage" ? score.level_index : 5;
20 |
21 | useEffect(() => {
22 | if (!song) return;
23 | const difficulty = getDifficulty(song, score.type, score.level_index);
24 | if (!difficulty) return;
25 | setIsBuddy(difficulty.is_buddy);
26 | if (score.type === "utage") return;
27 | setLevel(difficulty.level_value.toFixed(1));
28 | }, [song]);
29 |
30 | return <>
31 |
34 | {score.song_name}
35 | {score.type === "standard" && 标准}
36 | {score.type === "dx" && DX}
37 | {isBuddy && BUDDY}
38 |
39 |
42 |
43 | {score.achievements != -1 ? (
44 |
45 |
46 | {parseInt(String(score.achievements))}
47 | .{
48 | (String(score.achievements).split(".")[1] || "0").padEnd(4, "0")
49 | }%
50 |
51 |
52 | DX Rating: {deluxeRating}
53 |
54 |
55 | ) : (
56 |
57 |
58 | 未游玩
59 |
60 |
61 | 或未上传至查分器
62 |
63 |
64 | )}
65 |
66 |
69 | {level}
70 |
71 |
72 |
73 |
74 | >
75 | }
--------------------------------------------------------------------------------
/src/components/Scores/maimai/StatisticsSection.module.css:
--------------------------------------------------------------------------------
1 | .fullComboSyncSection {
2 | flex-direction: column;
3 |
4 | @media (max-width: 28rem) {
5 | flex-direction: row;
6 | }
7 |
8 | @media (max-width: 26rem) {
9 | flex-direction: column;
10 | }
11 | }
--------------------------------------------------------------------------------
/src/components/Settings/Settings.module.css:
--------------------------------------------------------------------------------
1 | .item {
2 | &:not(:last-child) {
3 | padding-bottom: var(--mantine-spacing-sm);
4 | }
5 | &:not(:first-child) {
6 | padding-top: var(--mantine-spacing-sm);
7 | }
8 | & + & {
9 | border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
10 | }
11 | }
12 |
13 | .switch {
14 | & * {
15 | cursor: pointer;
16 | }
17 | }
--------------------------------------------------------------------------------
/src/components/Settings/SettingsModal.tsx:
--------------------------------------------------------------------------------
1 | import { Modal } from "@mantine/core";
2 | import { SettingProps, SettingList } from "./SettingList.tsx";
3 |
4 | interface SettingsModalProps {
5 | title: string;
6 | data: SettingProps[];
7 | value?: any;
8 | opened: boolean;
9 | onClose: (value?: any) => void;
10 | onChange?: (key: string, value: any) => void;
11 | }
12 |
13 | export const SettingsModal = ({ title, data, value, opened, onClose, onChange }: SettingsModalProps) => {
14 | return (
15 |
16 |
17 |
18 |
19 | {title}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
--------------------------------------------------------------------------------
/src/components/Shell/Footer/Footer.module.css:
--------------------------------------------------------------------------------
1 | .link {
2 | color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
3 | font-size: var(--mantine-font-size-sm);
4 |
5 | &:hover {
6 | text-decoration: underline;
7 | }
8 | }
9 |
10 | .footer {
11 | border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
12 | }
13 |
14 | .footerInner {
15 | max-width: calc(896px + var(--mantine-spacing-xl) * 2);
16 | margin: 0 auto;
17 | padding: var(--mantine-spacing-xl);
18 |
19 | @mixin smaller-than $mantine-breakpoint-xs {
20 | flex-direction: column;
21 | }
22 | }
--------------------------------------------------------------------------------
/src/components/Shell/Footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { Divider, Flex, Group, Text } from "@mantine/core";
2 | import classes from './Footer.module.css';
3 |
4 | export const Footer = () => {
5 | return (
6 |
7 |
8 |
9 | maimai DX 查分器
10 |
11 |
12 | © {new Date().getFullYear() + ' '}
13 |
14 | component="a"
15 | className={classes.link}
16 | href="https://lxns.net/"
17 | target="_blank"
18 | >
19 | Lxns Network
20 |
21 |
22 |
23 |
24 | component="a"
25 | className={classes.link}
26 | size="sm"
27 | href="https://beian.miit.gov.cn/"
28 | target="_blank"
29 | >
30 | 粤ICP备18035696号
31 |
32 |
33 |
34 |
35 | )
36 | }
--------------------------------------------------------------------------------
/src/components/Shell/Header/ColorSchemeToggle.tsx:
--------------------------------------------------------------------------------
1 | import { ActionIcon, Group, rem, Tooltip, useMantineColorScheme } from "@mantine/core";
2 | import { IconMoonStars, IconSun, IconSunMoon } from "@tabler/icons-react";
3 |
4 |
5 | const colorSchemes = {
6 | auto: {
7 | icon: ,
8 | label: '跟随系统',
9 | color: 'blue'
10 | },
11 | dark: {
12 | icon: ,
13 | label: '深色模式',
14 | color: 'blue'
15 | },
16 | light: {
17 | icon: ,
18 | label: '浅色模式',
19 | color: 'yellow'
20 | }
21 | }
22 |
23 | export const ColorSchemeToggle = () => {
24 | const { colorScheme, setColorScheme } = useMantineColorScheme();
25 |
26 | return (
27 |
28 |
29 | setColorScheme(
30 | colorScheme === 'auto' ? 'dark' : colorScheme === 'dark' ? 'light' : 'auto'
31 | )} color={colorSchemes[colorScheme].color}>
32 | {colorSchemes[colorScheme].icon}
33 |
34 |
35 |
36 | );
37 | }
--------------------------------------------------------------------------------
/src/components/Shell/Header/GameTabs.module.css:
--------------------------------------------------------------------------------
1 | .tabs {
2 | position: relative;
3 |
4 | @mixin larger-than $mantine-breakpoint-xs {
5 | display: none !important;
6 | }
7 | }
8 |
9 | .tab {
10 | height: 32px;
11 | text-wrap: nowrap;
12 | border-radius: 4px 4px 0 0;
13 |
14 | &:hover {
15 | background: var(--mantine-color-default-hover);
16 | }
17 |
18 | &[data-active='true'] {
19 | color: var(--mantine-primary-color-light-color);
20 | }
21 | }
22 |
23 | .indicator {
24 | height: 33px !important;
25 | pointer-events: none;
26 | border-bottom: 1px solid var(--mantine-primary-color-light-color);
27 | }
--------------------------------------------------------------------------------
/src/components/Shell/Header/GameTabs.tsx:
--------------------------------------------------------------------------------
1 | import { FloatingIndicator, SimpleGrid, UnstyledButton } from "@mantine/core";
2 | import classes from "./GameTabs.module.css";
3 | import React, { useState } from "react";
4 |
5 | interface GameTabsProps {
6 | tabs: { id: string; name: string }[];
7 | activeTab: string;
8 | onTabChange(tab: string): void;
9 | style?: React.CSSProperties;
10 | }
11 |
12 | export const GameTabs = ({ tabs, activeTab, onTabChange, style }: GameTabsProps) => {
13 | const [rootRef, setRootRef] = useState(null);
14 | const [controlsRefs, setControlsRefs] = useState>({});
15 |
16 | const setControlRef = (index: string) => (node: HTMLButtonElement) => {
17 | controlsRefs[index] = node;
18 | setControlsRefs(controlsRefs);
19 | };
20 |
21 | return (
22 |
23 | {tabs.map((item) => (
24 | onTabChange(item.id)}
31 | mod={{ active: activeTab === item.id }}
32 | >
33 | {item.name}
34 |
35 | ))}
36 |
37 |
42 |
43 | );
44 | }
--------------------------------------------------------------------------------
/src/components/Shell/Header/Header.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | position: fixed;
3 | z-index: var(--mantine-z-index-modal);
4 | top: 0;
5 | width: 100%;
6 | padding: 0 var(--mantine-spacing-md);
7 | background-color: var(--mantine-color-body);
8 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
9 | }
10 |
11 | .navbarToggle {
12 | @mixin larger-than $mantine-breakpoint-md {
13 | display: none;
14 | }
15 | }
16 |
17 | .logo {
18 | display: flex;
19 | align-items: center;
20 | }
21 |
22 | .game {
23 | background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-4)) !important;
24 | font-weight: bold;
25 | display: flex;
26 | align-items: center;
27 | justify-content: center;
28 | line-height: 1;
29 | padding: 2px 8px 2px 12px !important;
30 | border-radius: var(--mantine-radius-xl);
31 |
32 | @mixin smaller-than $mantine-breakpoint-xs {
33 | display: none;
34 | }
35 |
36 | &:hover {
37 | background: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)) !important;
38 | }
39 | }
40 |
41 | .gameChevron {
42 | display: block;
43 | width: 14px;
44 | height: 14px;
45 | margin-left: 5px;
46 | }
--------------------------------------------------------------------------------
/src/components/Shell/Header/Logo.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Image, Text } from '@mantine/core'
2 | import { Link } from "react-router-dom";
3 |
4 | export default function Logo() {
5 | return (
6 |
17 |
18 |
23 | maimai DX 查分器
24 |
25 |
26 | );
27 | }
--------------------------------------------------------------------------------
/src/components/Shell/Navbar/Navbar.module.css:
--------------------------------------------------------------------------------
1 | .navbar {
2 | position: fixed;
3 | z-index: var(--mantine-z-index-modal);
4 | height: calc(100vh - var(--header-height));
5 | width: var(--navbar-width);
6 | background-color: var(--mantine-color-body);
7 | border-right: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
8 |
9 | display: flex;
10 | flex-direction: column;
11 |
12 | @mixin smaller-than $mantine-breakpoint-md {
13 | width: var(--navbar-width);
14 | }
15 |
16 | @supports (max-height: 100dvh) {
17 | height: calc(100dvh - var(--header-height));
18 | }
19 | }
20 |
21 | .navbarHeader {
22 | padding-bottom: var(--mantine-spacing-md);
23 | margin-bottom: calc(var(--mantine-spacing-md) * 1.5);
24 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
25 | }
26 |
27 | .navbarMain {
28 | flex: 1 1 auto;
29 | overflow-y: auto;
30 | }
31 |
32 | .navbarFooter {
33 | padding-top: var(--mantine-spacing-md);
34 | padding-bottom: var(--mantine-spacing-md);
35 | border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
36 | }
37 |
38 | .divider {
39 | color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
40 | }
41 |
42 | .navbarLink {
43 | display: flex;
44 | align-items: center;
45 | text-decoration: none;
46 | font-size: var(--mantine-font-size-sm);
47 | color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
48 | padding: 8px var(--mantine-spacing-sm);
49 | border-radius: var(--mantine-radius-md);
50 | font-weight: 500;
51 | cursor: pointer;
52 | -webkit-tap-highlight-color: transparent;
53 | transition: background-color 50ms ease-in-out, color 50ms ease-in-out;
54 |
55 | @mixin hover {
56 | background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
57 | color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
58 |
59 | .navbarLinkIcon {
60 | color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
61 | }
62 | }
63 |
64 | &[data-active] {
65 | &,
66 | &:hover {
67 | background-color: var(--mantine-primary-color-light);
68 | color: var(--mantine-primary-color-light-color);
69 |
70 | .navbarLinkIcon {
71 | color: var(--mantine-primary-color-light-color);
72 | }
73 | }
74 |
75 | @mixin hover {
76 | background-color: var(--mantine-primary-color-light-hover);
77 | }
78 | }
79 | }
80 |
81 | .navbarLinkIcon {
82 | color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
83 | display: flex;
84 | padding: 2px 0;
85 | transition: color 50ms ease-in-out;
86 | }
--------------------------------------------------------------------------------
/src/components/Shell/Navbar/NavbarButton.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from "react-router-dom";
2 | import { Badge, Group, Text } from "@mantine/core";
3 | import React from "react";
4 | import classes from './Navbar.module.css';
5 |
6 | interface NavbarButtonProps {
7 | label: string;
8 | icon: React.ReactNode;
9 | is_new?: boolean;
10 | to?: string;
11 | active?: string;
12 | onClose(): void;
13 | onClick?(): void;
14 | }
15 |
16 | export const NavbarButton = ({ label, icon, is_new, to, active, onClose, onClick }: NavbarButtonProps) => {
17 | const navigate = useNavigate();
18 |
19 | return (
20 | {
24 | event.preventDefault();
25 | onClick && onClick();
26 | if (to) navigate(to);
27 | onClose();
28 | }}
29 | >
30 |
31 | {icon}
32 | {label}
33 | {is_new && New}
36 |
37 |
38 | )
39 | }
--------------------------------------------------------------------------------
/src/components/Shell/Shell.module.css:
--------------------------------------------------------------------------------
1 | .routesWrapper {
2 | position: relative;
3 | height: calc(100vh - var(--header-height));
4 | box-sizing: border-box;
5 | margin-top: var(--header-height);
6 |
7 | @supports (max-height: 100dvh) {
8 | height: calc(100dvh - var(--header-height));
9 | }
10 | }
--------------------------------------------------------------------------------
/src/components/SongDisabledIndicator.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box, Flex, Indicator } from "@mantine/core";
3 | import { IconTrash } from "@tabler/icons-react";
4 |
5 | export const SongDisabledIndicator = ({ disabled, children }: { disabled?: boolean, children: React.ReactElement }) => {
6 | if (!disabled) {
7 | return children;
8 | }
9 | return 被移除}
13 | size={18}
14 | offset={12}
15 | zIndex={2}
16 | disabled={!disabled}
17 | >
18 |
25 | {children}
26 |
27 | }
--------------------------------------------------------------------------------
/src/components/Songs/SongCard.module.css:
--------------------------------------------------------------------------------
1 | @import '../../pages/Page.module.css';
2 |
3 | .jacket {
4 | position: relative;
5 |
6 | &::before {
7 | content: "";
8 | inset: calc(var(--scale) * -1.25rem * var(--mantine-scale));
9 | z-index: -1;
10 | position: absolute;
11 | background-image: linear-gradient(-45deg, var(--primary-color) 50%, var(--secondary-color) 0);
12 | filter: blur(72px);
13 | }
14 | }
15 |
16 | .audioPlayer {
17 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
18 | }
--------------------------------------------------------------------------------
/src/components/Songs/SongDifficulty.module.css:
--------------------------------------------------------------------------------
1 | .scoreCard {
2 | cursor: pointer;
3 | transition: transform 200ms ease;
4 |
5 | &:hover {
6 | transform: scale(1.03);
7 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
8 | box-shadow: var(--mantine-shadow-md);
9 | border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
10 | border-radius: var(--mantine-radius-md);
11 | z-index: 1;
12 | }
13 | }
14 |
15 | .scoreWorldsEnd {
16 | --angle: 0deg;
17 | }
18 |
19 | .scoreWorldsEnd::before {
20 | content: '';
21 | position: absolute;
22 | top: 0;
23 | left: 0;
24 | right: 0;
25 | bottom: 0;
26 | border-radius: var(--mantine-radius-md);
27 | padding: 2px;
28 | background: conic-gradient(from var(--angle), red, yellow, lime, aqua, blue, magenta, red);
29 | -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
30 | -webkit-mask-composite: xor;
31 | mask-composite: exclude;
32 | animation: 10s rotate linear infinite;
33 | }
34 |
35 | @keyframes rotate {
36 | to {
37 | --angle: 360deg;
38 | }
39 | }
40 |
41 | @property --angle {
42 | syntax: '';
43 | initial-value: 0deg;
44 | inherits: false;
45 | }
--------------------------------------------------------------------------------
/src/components/Sync/CopyButtonWithIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ActionIcon, CopyButton, TextInput, Tooltip } from "@mantine/core";
2 | import { IconCheck, IconCopy } from "@tabler/icons-react";
3 |
4 | export const CopyButtonWithIcon = ({ label, content, ...others }: any) => {
5 | return (
6 | e.target.select()}
10 | rightSection={
11 |
12 | {({ copied, copy }) => (
13 |
14 |
15 | {copied ? : }
16 |
17 |
18 | )}
19 |
20 | }
21 | readOnly
22 | {...others}
23 | />
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Sync/CrawlTokenAlert.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, Button, Text } from "@mantine/core";
2 | import { IconAlertCircle, IconRefresh } from "@tabler/icons-react";
3 |
4 | export const CrawlTokenAlert = ({ token, resetHandler }: any) => {
5 | const getExpireTime = (crawlToken: string) => {
6 | return Math.floor(((JSON.parse(atob(crawlToken.split('.')[1])).exp - new Date().getTime() / 1000)) / 60)
7 | }
8 |
9 | const isTokenExpired = token && getExpireTime(token) < 0;
10 | const alertColor = isTokenExpired ? 'yellow' : 'blue';
11 |
12 | return (
13 | } title="链接有效期提示" color={alertColor}>
14 |
15 | {token ? `该链接${
16 | isTokenExpired ? "已失效," : `将在 ${getExpireTime(token) + 1} 分钟内失效,逾时`
17 | }请点击下方按钮刷新 OAuth 链接。` : "链接未生成,请点击下方按钮生成 OAuth 链接。"}
18 |
19 | } onClick={resetHandler} color={alertColor}>
20 | {token ? "刷新链接" : "生成链接"}
21 |
22 |
23 | );
24 | };
--------------------------------------------------------------------------------
/src/components/Sync/WechatOAuthLink.tsx:
--------------------------------------------------------------------------------
1 | import { API_URL } from "../../main.tsx";
2 | import { Button, Group } from "@mantine/core";
3 | import { IconExternalLink } from "@tabler/icons-react";
4 | import { CopyButtonWithIcon } from "./CopyButtonWithIcon.tsx";
5 |
6 | export const WechatOAuthLink = ({ game = 'maimai', crawlToken }: { game: string, crawlToken: string | null }) => {
7 | const authLink = `${API_URL}/${game}/wechat/auth${crawlToken ? `?token=${window.btoa(crawlToken)}` : ''}`;
8 | const isMicroMessenger = /MicroMessenger/i.test(window.navigator.userAgent);
9 |
10 | return (
11 |
12 |
17 | {isMicroMessenger && (
18 | }
20 | onClick={() => window.open(authLink)}
21 | >
22 | 微信内跳转
23 |
24 | )}
25 |
26 | );
27 | };
--------------------------------------------------------------------------------
/src/components/YearInReview/SongRankingSection.module.css:
--------------------------------------------------------------------------------
1 | .jacket {
2 | position: relative;
3 |
4 | &::before {
5 | content: "";
6 | inset: calc(1.25rem * var(--mantine-scale));
7 | position: absolute;
8 | background-image: linear-gradient(-45deg, var(--primary-color) 50%, var(--secondary-color) 0);
9 | filter: blur(72px);
10 | }
11 | }
--------------------------------------------------------------------------------
/src/components/YearInReview/SongTimelineSection.module.css:
--------------------------------------------------------------------------------
1 | .jacket {
2 | position: relative;
3 |
4 | &::before {
5 | content: "";
6 | inset: calc(1.25rem * var(--mantine-scale));
7 | position: absolute;
8 | background-image: linear-gradient(-45deg, var(--primary-color) 50%, var(--secondary-color) 0);
9 | filter: blur(72px);
10 | }
11 | }
12 |
13 | .timeline {
14 | display: flex;
15 | flex-direction: column;
16 | }
17 |
18 | .timelineMonth {
19 | display: flex;
20 | flex-direction: column;
21 | width: calc(50% + 3px);
22 | align-items: center;
23 |
24 | @mixin smaller-than $mantine-breakpoint-xs {
25 | width: 100%;
26 | }
27 | }
28 |
29 | .timelineMonthTitle {
30 | font-size: 18px;
31 | font-weight: 500;
32 | text-transform: uppercase;
33 | margin-bottom: 10px;
34 | border-bottom: 2px solid var(--mantine-primary-color-filled);
35 | }
36 |
37 | .timelineMonthContent {
38 | display: flex;
39 | flex-direction: row;
40 | flex-wrap: wrap;
41 | justify-content: center;
42 | gap: 8px;
43 | width: 100%;
44 | font-size: 2em;
45 | }
46 |
47 | .timelineMonth:nth-child(odd) {
48 | align-self: flex-end;
49 | align-items: flex-start;
50 | border-left: 6px dotted var(--mantine-primary-color-filled);
51 | padding-left: 20px;
52 | box-sizing: border-box;
53 | margin-bottom: 4px;
54 |
55 | .timelineMonthTitle {
56 | margin-left: -22px;
57 | padding-left: 22px;
58 | }
59 |
60 | .timelineMonthContent {
61 | justify-content: flex-start;
62 | }
63 |
64 | @mixin smaller-than $mantine-breakpoint-xs {
65 | align-self: flex-start;
66 | }
67 | }
68 |
69 | .timelineMonth:nth-child(even) {
70 | align-self: flex-start;
71 | align-items: flex-end;
72 | border-right: 6px dotted var(--mantine-primary-color-filled);
73 | padding-right: 20px;
74 | box-sizing: border-box;
75 | margin-bottom: 4px;
76 |
77 | .timelineMonthTitle {
78 | margin-right: -22px;
79 | padding-right: 22px;
80 | }
81 |
82 | .timelineMonthContent {
83 | justify-content: flex-end;
84 | }
85 |
86 | @mixin smaller-than $mantine-breakpoint-xs {
87 | align-items: flex-start;
88 | border-left: 6px dotted var(--mantine-primary-color-filled);
89 | border-right: none;
90 | padding-left: 20px;
91 | padding-right: 0;
92 | box-sizing: border-box;
93 |
94 | .timelineMonthTitle {
95 | margin-left: -22px;
96 | margin-right: 0;
97 | padding-left: 22px;
98 | padding-right: 0;
99 | }
100 |
101 | .timelineMonthContent {
102 | justify-content: flex-start;
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------
/src/components/YearInReview/SongTimelineSection.tsx:
--------------------------------------------------------------------------------
1 | import { YearInReviewProps } from "@/pages/public/YearInReview.tsx";
2 | import classes from "./SongTimelineSection.module.css";
3 | import { ASSET_URL } from "@/main.tsx";
4 | import { IconPhotoOff } from "@tabler/icons-react";
5 | import { Avatar, Box } from "@mantine/core";
6 | import {useEffect, useState} from "react";
7 | import { ColorExtractor } from "react-color-extractor";
8 | import { Game } from "@/types/game";
9 | import LazyLoad, { forceCheck } from "react-lazyload";
10 | import useSongListStore from "@/hooks/useSongListStore.ts";
11 | import {useShallow} from "zustand/react/shallow";
12 |
13 | const SongImage = ({ game, id }: { game: Game, id: number }) => {
14 | const [colors, setColors] = useState([]);
15 |
16 | return (
17 | <>
18 | setColors(colors)}
21 | />
22 |
26 |
27 |
28 |
29 |
30 | >
31 | )
32 | }
33 |
34 | export const SongTimelineSection = ({ data }: { data: YearInReviewProps }) => {
35 | const { songList } = useSongListStore(
36 | useShallow((state) => ({ songList: state[data.game] })),
37 | );
38 |
39 | useEffect(() => {
40 | const scrollArea = document.querySelector(
41 | "#shell-root>.mantine-ScrollArea-root>.mantine-ScrollArea-viewport"
42 | );
43 |
44 | if (!scrollArea) return;
45 |
46 | forceCheck();
47 |
48 | scrollArea.addEventListener("scroll", forceCheck);
49 |
50 | return () => {
51 | scrollArea.removeEventListener("scroll", forceCheck);
52 | };
53 | }, []);
54 |
55 | return (
56 |
57 | {Object.entries(data.player_song_timeline).map(([month, songIds]) => (
58 |
59 |
{month} 月
60 |
61 | {songIds.map((id) => (
62 |
63 |
64 |
65 |
66 |
67 | ))}
68 |
69 |
70 | ))}
71 |
72 | )
73 | }
--------------------------------------------------------------------------------
/src/components/YearInReview/UploadRhythmSection.tsx:
--------------------------------------------------------------------------------
1 | import { YearInReviewProps } from "@/pages/public/YearInReview.tsx";
2 | import { BarChart, BubbleChart } from "@mantine/charts";
3 | import { Space } from "@mantine/core";
4 |
5 | export const UploadRhythmSection = ({ data }: { data: YearInReviewProps }) => {
6 | return (
7 | <>
8 | ({ key: `${key} 月`, value }))}
11 | dataKey="key"
12 | series={[
13 | { name: 'value', label: '次数', color: 'blue.6' },
14 | ]}
15 | unit=" 次"
16 | barProps={{ barSize: 50 }}
17 | />
18 |
19 | ({
22 | hour: key,
23 | index: 1,
24 | value
25 | }))}
26 | range={[16, 225]}
27 | label="上传量/时"
28 | color="lime.6"
29 | dataKey={{ x: 'hour', y: 'index', z: 'value' }}
30 | />
31 | >
32 | )
33 | }
--------------------------------------------------------------------------------
/src/components/YearInReview/YearSummarySection.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | height: 440px;
3 | padding: var(--mantine-spacing-xl);
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: space-between;
7 | align-items: flex-start;
8 | }
9 |
10 | .cardImage {
11 | background-size: cover;
12 | background-position: center;
13 |
14 | & .title {
15 | color: var(--mantine-color-white);
16 | }
17 |
18 | & .description {
19 | color: var(--mantine-color-white);
20 | }
21 | }
22 |
23 | .title {
24 | font-family:
25 | Greycliff CF,
26 | sans-serif;
27 | font-weight: 900;
28 | font-size: 32px;
29 | margin-top: var(--mantine-spacing-xs);
30 | }
31 |
32 | .description {
33 | opacity: 0.7;
34 | font-weight: 700;
35 | text-transform: uppercase;
36 | }
--------------------------------------------------------------------------------
/src/data/products.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "软糖酱 @LxBot",
4 | "tags": ["QQ 机器人", "舞萌 DX", "中二节奏"],
5 | "description": "你可以通过落雪咖啡屋提供的 LxBot QQ 机器人,查询你在 maimai DX 查分器中的游戏数据,使用我们精心设计的图片查询样式。",
6 | "image": "lxbot",
7 | "button": "添加",
8 | "url": "https://qun.qq.com/qunpro/robot/qunshare?robot_appid=102072150&robot_uin=2854207029"
9 | },
10 | {
11 | "title": "秋葉 @AkihaBot",
12 | "tags": ["Telegram 机器人", "舞萌 DX", "中二节奏"],
13 | "description": "秋葉是由 ☆ 开发的 Telegram 机器人,支持查询你在 maimai DX 查分器中的游戏数据并绘制最佳成绩图,也具有包括 maimai、CHUNITHM、Arcaea 在内的各种功能。",
14 | "image": "akihabot",
15 | "button": "添加",
16 | "url": "https://t.me/AkihaBot"
17 | },
18 | {
19 | "title": "FindMaimaiDX",
20 | "tags": ["手机应用", "舞萌 DX"],
21 | "description": "FindMaimaiDX 是由 Spasol 开发的一款以寻找附近机厅为主要功能的软件,支持 Best 50 歌曲信息具体查看,店铺“附近商店”及“导航”和“可视化地图”等多项功能。",
22 | "image": "findmaimaidx",
23 | "button": "了解更多",
24 | "url": "https://github.com/Spaso1/FindMaimaiDX_Phone"
25 | },
26 | {
27 | "title": "MaiProberPlus",
28 | "tags": ["手机应用", "舞萌 DX", "中二节奏"],
29 | "description": "基于 Android VpnService 的分数上传器,旨在帮助用户在各种环境下更方便的上传分数至目标查分器。查分器还支持本地分数个性化管理,Best 50 生成。",
30 | "image": "maiproberplus",
31 | "button": "了解更多",
32 | "url": "https://github.com/SkyDynamic/MaiproberPlus"
33 | },
34 | {
35 | "title": "妖梦 @youmu-bot",
36 | "tags": ["QQ 机器人", "舞萌 DX"],
37 | "description": "基于 NapCat + OverFlow 的机器人,提供了舞萌玩家最常用的一些功能。",
38 | "image": "youmubot",
39 | "button": "了解更多",
40 | "url": "https://youmu.kagg886.top"
41 | }
42 | ]
--------------------------------------------------------------------------------
/src/hooks/swr/fetcher.ts:
--------------------------------------------------------------------------------
1 | import { fetchAPI } from "@/utils/api/api.ts";
2 |
3 | export const fetcher = async (url: string) => {
4 | const res = await fetchAPI(url, { method: "GET" });
5 | const data = await res.json();
6 | if (!data.success) {
7 | throw new Error(data.message);
8 | }
9 | return data.data;
10 | }
--------------------------------------------------------------------------------
/src/hooks/swr/useAliasVotes.ts:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 | import { fetcher } from "@/hooks/swr/fetcher.ts";
3 | import { VoteProps } from "@/types/alias";
4 | import { Game } from "@/types/game";
5 |
6 | export const useAliasVotes = (game: Game) => {
7 | const {
8 | data,
9 | error,
10 | isLoading,
11 | mutate
12 | } = useSWR(`user/${game}/alias/votes`, fetcher);
13 |
14 | return {
15 | votes: data || [],
16 | isLoading: isLoading,
17 | error: error,
18 | mutate: mutate,
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/src/hooks/swr/useAliases.ts:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 | import { fetcher } from "@/hooks/swr/fetcher.ts";
3 | import { AliasListProps } from "@/types/alias";
4 | import { Game } from "@/types/game";
5 |
6 | export const useAliases = (game: Game, page: number, approved: boolean = false, sort: string = "alias_id", order: string = "desc", songId: number = 0) => {
7 | const params = new URLSearchParams({
8 | page: String(page),
9 | sort: `${sort} ${order}`,
10 | approved: String(approved)
11 | });
12 |
13 | if (songId !== 0) {
14 | params.append("song_id", String(songId));
15 | }
16 |
17 | const {
18 | data,
19 | error,
20 | isLoading,
21 | mutate
22 | } = useSWR(`user/${game}/alias/list?${params.toString()}`, fetcher);
23 |
24 | return {
25 | aliases: data ? data.aliases : [],
26 | pageCount: data ? data.page_count : 0,
27 | pageSize: data ? data.page_size : 0,
28 | isLoading: isLoading,
29 | error: error,
30 | mutate: mutate,
31 | };
32 | };
33 |
--------------------------------------------------------------------------------
/src/hooks/swr/useBests.ts:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 | import { fetcher } from "@/hooks/swr/fetcher.ts";
3 | import { ChunithmBestsProps, MaimaiBestsProps } from "@/types/score";
4 | import { Game } from "@/types/game";
5 |
6 | export const useBests = (game: Game) => {
7 | const {
8 | data,
9 | error,
10 | isLoading,
11 | mutate
12 | } = useSWR(`user/${game}/player/bests`, fetcher);
13 |
14 | return {
15 | bests: data,
16 | isLoading: isLoading,
17 | error: error,
18 | mutate: mutate,
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/src/hooks/swr/usePlayer.ts:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 | import { fetcher } from "@/hooks/swr/fetcher.ts";
3 | import { MaimaiPlayerProps, ChunithmPlayerProps } from "@/types/player";
4 | import { Game } from "@/types/game";
5 |
6 | export const usePlayer = (game: Game) => {
7 | const {
8 | data,
9 | error,
10 | isLoading,
11 | mutate
12 | } = useSWR(`user/${game}/player`, fetcher);
13 |
14 | return {
15 | player: data,
16 | isLoading: isLoading,
17 | error: error,
18 | mutate: mutate,
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/src/hooks/swr/useScores.ts:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 | import { fetcher } from "@/hooks/swr/fetcher.ts";
3 | import { ChunithmScoreProps, MaimaiScoreProps } from "@/types/score";
4 | import { Game } from "@/types/game";
5 |
6 | export const useScores = (game: Game) => {
7 | const {
8 | data,
9 | error,
10 | isLoading,
11 | mutate
12 | } = useSWR<(MaimaiScoreProps | ChunithmScoreProps)[]>(`user/${game}/player/scores`, fetcher);
13 |
14 | return {
15 | scores: data || [],
16 | isLoading: isLoading,
17 | error: error,
18 | mutate: mutate,
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/src/hooks/swr/useSiteConfig.ts:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 | import { fetcher } from "@/hooks/swr/fetcher.ts";
3 |
4 | interface ConfigProps {
5 | resource_hashes: {
6 | [key: string]: {
7 | [key: string]: string;
8 | }
9 | }
10 | }
11 |
12 | export const useSiteConfig = () => {
13 | const {
14 | data,
15 | error,
16 | isLoading,
17 | mutate
18 | } = useSWR(`site/config`, fetcher, {
19 | revalidateIfStale: false,
20 | revalidateOnFocus: false,
21 | revalidateOnReconnect: false
22 | });
23 |
24 | return {
25 | config: data,
26 | isLoading: isLoading,
27 | error: error,
28 | mutate: mutate,
29 | };
30 | };
31 |
--------------------------------------------------------------------------------
/src/hooks/swr/useUser.ts:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 | import { fetcher } from "@/hooks/swr/fetcher.ts";
3 | import { UserProps } from "@/types/user";
4 |
5 | export const useUser = () => {
6 | const {
7 | data,
8 | error,
9 | isLoading,
10 | mutate
11 | } = useSWR(`user/profile`, fetcher);
12 |
13 | return {
14 | user: data,
15 | isLoading: isLoading,
16 | error: error,
17 | mutate: mutate,
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/src/hooks/swr/useUserConfig.ts:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 | import { fetcher } from "@/hooks/swr/fetcher.ts";
3 | import { ConfigProps } from "@/types/user";
4 | import { Game } from "@/types/game";
5 |
6 | export const useUserConfig = (game: Game) => {
7 | const {
8 | data,
9 | error,
10 | isLoading,
11 | mutate
12 | } = useSWR(`user/${game}/config`, fetcher);
13 |
14 | return {
15 | config: data,
16 | isLoading: isLoading,
17 | error: error,
18 | mutate: mutate,
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/src/hooks/swr/useUserToken.ts:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 | import { fetcher } from "@/hooks/swr/fetcher.ts";
3 | import { isTokenExpired, isTokenUndefined } from "@/utils/session.ts";
4 |
5 | export const useUserToken = () => {
6 | const {
7 | data,
8 | error,
9 | isLoading,
10 | mutate
11 | } = useSWR(!isTokenUndefined() && `user/refresh`, fetcher);
12 |
13 | if (data) {
14 | localStorage.setItem("token", data.token);
15 | }
16 |
17 | if (!isTokenExpired()) {
18 | return {
19 | token: localStorage.getItem("token") || "",
20 | isLoading: false,
21 | error: null,
22 | mutate: () => {},
23 | };
24 | }
25 |
26 | return {
27 | token: data ? data.token : "",
28 | isLoading: isLoading,
29 | error: error,
30 | mutate: mutate,
31 | };
32 | };
33 |
--------------------------------------------------------------------------------
/src/hooks/swr/useYearInReview.ts:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 | import { fetcher } from "@/hooks/swr/fetcher.ts";
3 | import { Game } from "@/types/game";
4 | import { YearInReviewProps } from "@/pages/public/YearInReview.tsx";
5 |
6 | export const useYearInReview = (game: Game, year: number, shareToken?: string, agree?: boolean) => {
7 | let url;
8 | if (shareToken) {
9 | url = `${game}/year-in-review/${year}/share/${shareToken}`;
10 | } else {
11 | url = `user/${game}/player/year-in-review/${year}`
12 | }
13 | if (agree) {
14 | url += "?agree=true";
15 | }
16 | const {
17 | data,
18 | error,
19 | isLoading,
20 | mutate
21 | } = useSWR(url, fetcher, {
22 | revalidateIfStale: false,
23 | revalidateOnFocus: false,
24 | revalidateOnReconnect: false,
25 | onErrorRetry: (error) => {
26 | if (error.status === 400) return;
27 | }
28 | });
29 |
30 | return {
31 | data: data,
32 | isLoading: isLoading,
33 | error: error,
34 | mutate: mutate,
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/src/hooks/useAliasListStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 | import { AliasList } from "../utils/api/alias.ts";
3 | import { Game } from "@/types/game";
4 |
5 | interface AliasListState {
6 | maimai: AliasList,
7 | chunithm: AliasList,
8 | getAliasList: (game: Game) => AliasList,
9 | fetchAliasList: () => Promise,
10 | }
11 |
12 | const useAliasListStore = create((set, get) => ({
13 | maimai: new AliasList('maimai'),
14 | chunithm: new AliasList('chunithm'),
15 | getAliasList: (game) => get()[game],
16 | fetchAliasList: async () => {
17 | await Promise.all([get().maimai.fetch(), get().chunithm.fetch()]);
18 |
19 | set({
20 | getAliasList: (game: Game) => get()[game],
21 | });
22 | },
23 | }))
24 |
25 | export default useAliasListStore;
--------------------------------------------------------------------------------
/src/hooks/useFixedGame.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Game } from "@/types/game";
3 |
4 | function useFixedGame(defaultGame = 'maimai'): [Game, (game: Game) => void] {
5 | const [game, setGame] = useState(() => {
6 | const storedGame = localStorage.getItem('game');
7 | return storedGame ? JSON.parse(storedGame) : defaultGame;
8 | });
9 |
10 | return [game, setGame];
11 | }
12 |
13 | export default useFixedGame;
--------------------------------------------------------------------------------
/src/hooks/useGame.ts:
--------------------------------------------------------------------------------
1 | import { Game } from "@/types/game";
2 | import { useLocalStorage } from "@mantine/hooks";
3 | import { useSearchParams } from "react-router-dom";
4 |
5 | function useGame(defaultGame: Game = 'maimai'): [Game, (game: Game) => void] {
6 | const [searchParams] = useSearchParams();
7 |
8 | const gameFromSearch = searchParams.get("game");
9 | const validGames = ["maimai", "chunithm"];
10 | const gameFromStorage = localStorage.getItem("game");
11 |
12 | const initialGame: Game =
13 | validGames.includes(gameFromSearch || "") ? (gameFromSearch as Game) :
14 | (gameFromStorage ? (JSON.parse(gameFromStorage) as Game) : defaultGame);
15 |
16 | if (initialGame !== gameFromStorage) {
17 | localStorage.setItem("game", JSON.stringify(initialGame));
18 | }
19 |
20 | const [game, setGame] = useLocalStorage({
21 | key: "game",
22 | defaultValue: initialGame,
23 | });
24 |
25 | return [game, setGame];
26 | }
27 |
28 | export default useGame;
29 |
--------------------------------------------------------------------------------
/src/hooks/useShellViewportSize.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | function useShellViewportSize(): { width: number, height: number } {
4 | const [size, setSize] = useState({ width: 0, height: 0 });
5 |
6 | useEffect(() => {
7 | const scrollArea = document.querySelector(
8 | "#shell-root>.mantine-ScrollArea-root>.mantine-ScrollArea-viewport"
9 | );
10 |
11 | const updateSize = () => {
12 | if (scrollArea) {
13 | setSize({ width: scrollArea.clientWidth, height: scrollArea.clientHeight });
14 | }
15 | };
16 |
17 | updateSize();
18 |
19 | window.addEventListener("resize", updateSize);
20 |
21 | return () => {
22 | window.removeEventListener("resize", updateSize);
23 | };
24 | }, []);
25 |
26 | return size;
27 | }
28 |
29 | export default useShellViewportSize;
30 |
--------------------------------------------------------------------------------
/src/hooks/useSongListStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 | import { MaimaiSongList } from "../utils/api/song/maimai.ts";
3 | import { ChunithmSongList } from "../utils/api/song/chunithm.ts";
4 | import { Game } from "@/types/game";
5 |
6 | type SongListState = {
7 | maimai: MaimaiSongList,
8 | chunithm: ChunithmSongList,
9 | getSongList: (game: Game) => MaimaiSongList | ChunithmSongList,
10 | fetchSongList: (hashes?: {
11 | [key: string]: {
12 | [key: string]: string;
13 | }
14 | }) => Promise,
15 | }
16 |
17 | const useSongListStore = create((set, get) => ({
18 | maimai: new MaimaiSongList(),
19 | chunithm: new ChunithmSongList(),
20 | getSongList: (game) => get()[game],
21 | fetchSongList: async (hashes) => {
22 | await Promise.all([
23 | get().maimai.fetch(hashes?.maimai.songs),
24 | get().chunithm.fetch(hashes?.chunithm.songs),
25 | ]);
26 |
27 | set((state) => ({
28 | getSongList: (game: Game) => state[game],
29 | }));
30 | },
31 | }));
32 |
33 | export default useSongListStore;
--------------------------------------------------------------------------------
/src/hooks/useVersionChecker.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 | import { notifications } from "@mantine/notifications";
3 | import { Button, Stack } from "@mantine/core";
4 | import useSWR from 'swr';
5 |
6 | const fetcher = (url: string) =>
7 | fetch(url + '?_t=' + Date.now()).then((res) => res.json());
8 |
9 | export function useVersionChecker(interval = 10000) {
10 | const currentVersionRef = useRef(null);
11 | const notifiedRef = useRef(false);
12 |
13 | const isProd = import.meta.env.PROD;
14 | const { data } = useSWR<{ version: string }>(
15 | isProd ? '/version.json' : null,
16 | fetcher,
17 | {
18 | refreshInterval: interval,
19 | revalidateOnFocus: true,
20 | shouldRetryOnError: true,
21 | }
22 | );
23 |
24 | useEffect(() => {
25 | if (!isProd || !data?.version) return;
26 |
27 | if (!currentVersionRef.current) {
28 | currentVersionRef.current = data.version;
29 | } else if (
30 | currentVersionRef.current !== data.version &&
31 | !notifiedRef.current
32 | ) {
33 | notifiedRef.current = true;
34 |
35 | notifications.show({
36 | title: '新版本可用',
37 | message: (
38 |
39 | 检测到新版本,请刷新页面以获取最新版本
40 |
49 |
50 | ),
51 | color: 'blue',
52 | autoClose: false,
53 | });
54 | }
55 | }, [data, isProd]);
56 | }
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 |
9 | text-rendering: optimizeLegibility;
10 | -webkit-font-smoothing: antialiased;
11 | -moz-osx-font-smoothing: grayscale;
12 | -webkit-text-size-adjust: 100%;
13 | }
14 |
15 | button:focus,
16 | button:focus-visible {
17 | outline: none;
18 | }
19 |
20 | img {
21 | opacity: light-dark(1, 0.8);
22 | }
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import './wdyr';
2 |
3 | import React from 'react'
4 | import ReactDOM from 'react-dom/client'
5 | import { RouterProvider } from "react-router-dom";
6 | import { router } from './router';
7 | import { Helmet } from "react-helmet";
8 |
9 | import "@mantine/core/styles.css";
10 | import "@mantine/dates/styles.css";
11 | import "@mantine/nprogress/styles.css";
12 | import "@mantine/notifications/styles.css";
13 | import '@mantine/carousel/styles.css';
14 | import '@mantine/tiptap/styles.css';
15 | import '@mantine/charts/styles.css';
16 | import "mantine-datatable/styles.css";
17 | import "./index.css";
18 |
19 | export const API_URL = import.meta.env.VITE_API_URL;
20 | export const ASSET_URL = import.meta.env.VITE_ASSET_URL;
21 | export const RECAPTCHA_SITE_KEY = import.meta.env.VITE_RECAPTCHA_SITE_KEY;
22 |
23 | window.addEventListener('vite:preloadError', () => {
24 | window.location.reload()
25 | })
26 |
27 | ReactDOM.createRoot(document.getElementById('root')!).render(
28 |
29 |
30 | maimai DX 查分器
31 |
33 |
35 |
36 |
37 |
38 | ,
39 | )
40 |
--------------------------------------------------------------------------------
/src/pages/Form.module.css:
--------------------------------------------------------------------------------
1 | .root {
2 | padding-top: 80px;
3 | padding-bottom: 80px;
4 | }
5 |
6 | .card {
7 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
8 | }
9 |
10 | .highlight {
11 | position: relative;
12 | background-color: var(--mantine-primary-color-light);
13 | border-radius: var(--mantine-radius-sm);
14 | padding: 4px 8px;
15 | color: light-dark(var(--mantine-primary-color-6), var(--mantine-primary-color-4));
16 | }
--------------------------------------------------------------------------------
/src/pages/Page.module.css:
--------------------------------------------------------------------------------
1 | .root {
2 | padding: 16px;
3 | max-width: 600px;
4 | }
5 |
6 | .card {
7 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
8 | }
9 |
10 | .section {
11 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
12 | padding: var(--mantine-spacing-md);
13 | }
--------------------------------------------------------------------------------
/src/pages/admin/Panel/Panel.tsx:
--------------------------------------------------------------------------------
1 | import { Page } from "@/components/Page/Page.tsx";
2 | import { AdminUsersSection } from "@/pages/admin/Panel/users/AdminUsersSection.tsx";
3 | import { AdminDevelopersSection } from "@/pages/admin/Panel/developers/AdminDevelopersSection.tsx";
4 |
5 | export function Panel() {
6 | return (
7 | },
14 | { id: "developers", name: "开发者列表", children: },
15 | ]}
16 | />
17 | );
18 | }
--------------------------------------------------------------------------------
/src/pages/admin/Panel/developers/AdminDevelopersSection.module.css:
--------------------------------------------------------------------------------
1 | @import "@/pages/Page.module.css";
2 |
3 | .section {
4 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
5 | padding: var(--mantine-spacing-md);
6 | }
7 |
8 | .user {
9 | display: block;
10 | width: 100%;
11 | padding: var(--mantine-spacing-md);
12 | color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
13 |
14 | &:hover {
15 | background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
16 | }
17 | }
--------------------------------------------------------------------------------
/src/pages/admin/Panel/index.ts:
--------------------------------------------------------------------------------
1 | import { Panel } from './Panel';
2 |
3 | export default Panel;
--------------------------------------------------------------------------------
/src/pages/public/ErrorPage.module.css:
--------------------------------------------------------------------------------
1 | .title {
2 | text-align: center;
3 | font-weight: 800;
4 | letter-spacing: -1px;
5 | margin-bottom: var(--mantine-spacing-xs);
6 | color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
7 |
8 | @mixin smaller-than $mantine-breakpoint-xs {
9 | font-size: 28px !important;
10 | }
11 | }
12 |
13 | .description {
14 | text-align: center;
15 |
16 | @mixin smaller-than $mantine-breakpoint-xs {
17 | font-size: var(--mantine-font-size-md) !important;
18 | }
19 | }
20 |
21 | .control {
22 | @mixin smaller-than $mantine-breakpoint-xs {
23 | height: 42px !important;
24 | font-size: var(--mantine-font-size-md) !important;
25 | }
26 | }
--------------------------------------------------------------------------------
/src/pages/public/Fallback.tsx:
--------------------------------------------------------------------------------
1 | import { Center, Container, Button, Group, Text, Title, Image, Collapse, Code, ScrollArea } from "@mantine/core";
2 | import { useDisclosure, useViewportSize } from "@mantine/hooks";
3 | import classes from "./ErrorPage.module.css";
4 |
5 | export function Fallback({ error, resetErrorBoundary }: { error: Error, resetErrorBoundary: () => void }) {
6 | const { width } = useViewportSize();
7 | const [opened, { toggle }] = useDisclosure(false);
8 |
9 | return (
10 |
11 |
12 |
13 |
14 | 发生意料之外的错误
15 |
16 | {error.message}
17 |
18 |
19 |
20 |
21 |
22 | {error.stack}
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 |
34 | );
35 | }
--------------------------------------------------------------------------------
/src/pages/public/Home.module.css:
--------------------------------------------------------------------------------
1 | .root {
2 | position: relative;
3 | padding-top: 40px;
4 | padding-bottom: 100px;
5 |
6 | @mixin smaller-than $mantine-breakpoint-xs {
7 | padding-top: 16px;
8 | }
9 | }
10 |
11 | .section {
12 | @mixin smaller-than $mantine-breakpoint-xs {
13 | padding: 0;
14 | }
15 | }
16 |
17 | .title {
18 | text-align: center;
19 | font-weight: 800;
20 | font-size: 40px;
21 | letter-spacing: -1px;
22 | margin-bottom: var(--mantine-spacing-xs);
23 |
24 | @mixin smaller-than $mantine-breakpoint-xs {
25 | font-size: 28px;
26 | text-align: left;
27 | }
28 | }
29 |
30 | .description {
31 | text-align: center;
32 |
33 | @mixin smaller-than $mantine-breakpoint-xs {
34 | text-align: left;
35 | font-size: var(--mantine-font-size-md);
36 | }
37 | }
38 |
39 | .highlight {
40 | display: inline-block;
41 | position: relative;
42 | line-height: 1;
43 | padding: 0 8px;
44 | color: light-dark(var(--mantine-primary-color-6), var(--mantine-primary-color-4));
45 | }
46 |
47 | .highlight::before {
48 | position: absolute;
49 | content: '';
50 | height: calc(100% + 12px);
51 | width: 100%;
52 | left: 0;
53 | top: -6px;
54 | background-color: var(--mantine-primary-color-light);
55 | border-radius: var(--mantine-radius-sm);
56 | }
57 |
58 | .controls {
59 | margin-top: var(--mantine-spacing-lg);
60 | display: flex;
61 | justify-content: center;
62 |
63 | @mixin smaller-than $mantine-breakpoint-xs {
64 | flex-direction: column;
65 | }
66 | }
67 |
68 | .control {
69 | &:not(:first-of-type) {
70 | margin-left: var(--mantine-spacing-md);
71 |
72 | @mixin smaller-than $mantine-breakpoint-xs {
73 | margin-left: 0;
74 | margin-top: var(--mantine-spacing-md);
75 | }
76 | }
77 |
78 | @mixin smaller-than $mantine-breakpoint-xs {
79 | height: 42px;
80 | font-size: var(--mantine-font-size-md);
81 | }
82 | }
83 |
84 | .featureTitle {
85 | color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
86 | }
--------------------------------------------------------------------------------
/src/pages/public/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import { Container, Title, Text, Button, Group, Center, Image } from '@mantine/core';
2 | import { useNavigate } from "react-router-dom";
3 | import { useEffect } from "react";
4 | import classes from "./ErrorPage.module.css";
5 |
6 | export default function NotFound() {
7 | const navigate = useNavigate();
8 |
9 | useEffect(() => {
10 | document.title = "页面不存在 | maimai DX 查分器";
11 | });
12 |
13 | return (
14 |
15 |
16 |
17 |
18 | 页面不存在
19 |
20 | 你访问的页面不存在,可能已经被删除或者地址错误。
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
--------------------------------------------------------------------------------
/src/pages/public/YearInReview.module.css:
--------------------------------------------------------------------------------
1 | .root {
2 | position: relative;
3 | padding-top: 100px;
4 | padding-bottom: 100px;
5 |
6 | @mixin smaller-than $mantine-breakpoint-xs {
7 | padding-top: 50px;
8 | padding-bottom: 50px;
9 | }
10 | }
11 |
12 | .section {
13 | @mixin smaller-than $mantine-breakpoint-xs {
14 | padding: 0;
15 | }
16 | }
17 |
18 | .playerTitle > .highlight {
19 | font-size: 32px;
20 |
21 | @mixin smaller-than $mantine-breakpoint-xs {
22 | font-size: 20px;
23 | }
24 | }
25 |
26 | .playerTitle > h1 {
27 | font-size: 48px;
28 |
29 | @mixin smaller-than $mantine-breakpoint-xs {
30 | font-size: 24px;
31 | }
32 | }
33 |
34 | .title {
35 | font-weight: 800;
36 | font-size: 64px;
37 | letter-spacing: -1px;
38 | margin-bottom: var(--mantine-spacing-xs);
39 |
40 | @mixin smaller-than $mantine-breakpoint-xs {
41 | font-size: 32px;
42 | text-align: left;
43 | margin-top: var(--mantine-spacing-md);
44 | }
45 | }
46 |
47 | .description {
48 | @mixin smaller-than $mantine-breakpoint-xs {
49 | text-align: left;
50 | font-size: var(--mantine-font-size-md);
51 | }
52 | }
53 |
54 | .highlight {
55 | display: inline-block;
56 | position: relative;
57 | line-height: 1;
58 | padding: 0 8px;
59 | color: light-dark(var(--mantine-primary-color-6), var(--mantine-primary-color-4));
60 | }
61 |
62 | .highlight::before {
63 | position: absolute;
64 | content: '';
65 | height: calc(100% + 12px);
66 | width: 100%;
67 | left: 0;
68 | top: -6px;
69 | background-color: var(--mantine-primary-color-light);
70 | border-radius: var(--mantine-radius-sm);
71 | }
72 |
73 | .arrow {
74 | animation: arrow 1s infinite;
75 | }
76 |
77 | @keyframes arrow {
78 | 0% {
79 | transform: translateY(5px);
80 | }
81 |
82 | 50% {
83 | transform: translateY(-5px);
84 | }
85 |
86 | 100% {
87 | transform: translateY(5px);
88 | }
89 | }
--------------------------------------------------------------------------------
/src/pages/user/Profile.tsx:
--------------------------------------------------------------------------------
1 | import { Group, Loader, Space } from '@mantine/core';
2 | import { PlayerSection } from '../../components/Profile/PlayerSection';
3 | import { UserSection } from '../../components/Profile/UserSection';
4 | import { UserBindSection } from '../../components/Profile/UserBindSection';
5 | import { UserTokenSection } from "../../components/Profile/UserTokenSection.tsx";
6 | import { Page } from "@/components/Page/Page.tsx";
7 | import { useUser } from "@/hooks/swr/useUser.ts";
8 |
9 | const ProfileContent = () => {
10 | const { isLoading } = useUser();
11 |
12 | if (isLoading) {
13 | return (
14 |
15 |
16 |
17 | )
18 | }
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | export default function Profile() {
34 | return (
35 | }
41 | />
42 | )
43 | }
--------------------------------------------------------------------------------
/src/pages/user/Scores/Scores.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import ScoreContext from "@/utils/context.ts";
3 | import { Page } from "@/components/Page/Page.tsx";
4 | import { ScoreBestsSection } from "./bests/ScoreBestsSection.tsx";
5 | import { ScoreBackupSection } from "./backup/ScoreBackupSection.tsx";
6 | import { ScoreListSection } from "./list/ScoreListSection.tsx";
7 | import { ChunithmScoreProps, MaimaiScoreProps } from "@/types/score";
8 |
9 | export function Scores() {
10 | const [score, setScore] = useState(null);
11 | const [createScoreOpened, setCreateScoreOpened] = useState(false);
12 |
13 | return (
14 |
15 | },
22 | { id: "bests", name: "分数构成", children: },
23 | { id: "backup", name: "备份成绩", children: }
24 | ]}
25 | />
26 |
27 | );
28 | }
--------------------------------------------------------------------------------
/src/pages/user/Scores/backup/ScoreBackupSection.module.css:
--------------------------------------------------------------------------------
1 | .cardButton {
2 | &:hover {
3 | background-color: var(--mantine-color-default-hover);
4 | }
5 |
6 | &[data-disabled] {
7 | opacity: 0.5;
8 | pointer-events: none;
9 | }
10 | }
--------------------------------------------------------------------------------
/src/pages/user/Scores/bests/ScoreBestsSection.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Flex, Group, Loader, Space, Text, Title } from "@mantine/core";
2 | import { ScoreList } from "@/components/Scores/ScoreList.tsx";
3 | import { useBests } from "@/hooks/swr/useBests.ts";
4 | import { IconDatabaseOff } from "@tabler/icons-react";
5 | import { RatingSegments } from "@/components/Scores/RatingSegments.tsx";
6 | import useGame from "@/hooks/useGame.ts";
7 |
8 | export const ScoreBestsSection = () => {
9 | const [game] = useGame();
10 |
11 | const { bests, isLoading } = useBests(game);
12 |
13 | if (isLoading) {
14 | return (
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | if (!bests) {
22 | return (
23 |
24 |
25 | 没有获取到任何最佳成绩
26 |
27 | );
28 | }
29 |
30 | return (
31 | <>
32 |
33 |
34 | {"dx" in bests && (
35 |
36 | Best 15
37 | 现版本最佳曲目
38 |
39 |
40 | )}
41 | {"standard" in bests && (
42 |
43 | Best 35
44 | 旧版本最佳曲目
45 |
46 |
47 | )}
48 | {"bests" in bests && (
49 |
50 | Best 30
51 | 最佳曲目
52 |
53 |
54 | )}
55 | {"selections" in bests && (
56 |
57 | Selection 10
58 | 候选最佳曲目
59 |
60 |
61 | )}
62 | {"recents" in bests && (
63 |
64 | Recent 10
65 | 最近游玩的最佳曲目
66 |
67 |
68 | )}
69 | >
70 | )
71 | }
--------------------------------------------------------------------------------
/src/pages/user/Scores/index.ts:
--------------------------------------------------------------------------------
1 | import { Scores } from './Scores';
2 |
3 | export default Scores;
--------------------------------------------------------------------------------
/src/pages/user/Sync.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
3 | }
4 |
5 | .section {
6 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
7 | padding: var(--mantine-spacing-md);
8 | }
9 |
10 | .loaderText {
11 | & + & {
12 | padding-top: var(--mantine-spacing-sm);
13 | margin-top: var(--mantine-spacing-sm);
14 | border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
15 | }
16 | }
17 |
18 | .subParameters {
19 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
20 | padding: 6px 12px;
21 | border-radius: var(--mantine-radius-md);
22 | }
--------------------------------------------------------------------------------
/src/types/alias.d.ts:
--------------------------------------------------------------------------------
1 | export interface AliasListProps {
2 | aliases: AliasProps[];
3 | page_count: number;
4 | page_size: number;
5 | }
6 |
7 | export interface AliasProps {
8 | alias_id: number;
9 | song: {
10 | id: number;
11 | name: string;
12 | };
13 | song_type: string;
14 | difficulty: number;
15 | alias: string;
16 | approved: boolean;
17 | weight: {
18 | up: number;
19 | down: number;
20 | total: number;
21 | };
22 | uploader: {
23 | id: number;
24 | name: string;
25 | };
26 | upload_time: string;
27 | // extra
28 | vote?: VoteProps;
29 | }
30 |
31 | export interface VoteProps {
32 | alias_id?: number;
33 | vote_id?: number;
34 | weight: number;
35 | }
--------------------------------------------------------------------------------
/src/types/game.d.ts:
--------------------------------------------------------------------------------
1 | function strEnum(o: Array): { [K in T]: K } {
2 | return o.reduce((res, key) => {
3 | res[key] = key;
4 | return res;
5 | }, Object.create(null));
6 | }
7 |
8 | const Game = strEnum(['maimai', 'chunithm']);
9 |
10 | type Game = keyof typeof Game;
11 |
12 | export { Game };
--------------------------------------------------------------------------------
/src/types/player.d.ts:
--------------------------------------------------------------------------------
1 | export interface MaimaiPlayerProps {
2 | name: string;
3 | rating: number;
4 | friend_code: number;
5 | trophy?: {
6 | name: string;
7 | color: string;
8 | };
9 | course_rank: number;
10 | class_rank: number;
11 | star: number;
12 | icon?: CollectionProps;
13 | name_plate?: CollectionProps;
14 | frame?: CollectionProps;
15 | upload_time: string;
16 | }
17 |
18 | export interface ChunithmPlayerProps {
19 | name: string;
20 | level: number;
21 | rating: number;
22 | friend_code: number;
23 | class_emblem: {
24 | base: number;
25 | medal: number;
26 | };
27 | reborn_count: number;
28 | trophy: {
29 | name: string;
30 | color: string;
31 | };
32 | over_power: number;
33 | over_power_progress: number;
34 | currency: number;
35 | total_currency: number;
36 | character?: CollectionProps;
37 | name_plate?: CollectionProps;
38 | map_icon?: CollectionProps;
39 | upload_time: string;
40 | }
41 |
42 | interface CollectionProps {
43 | id: number;
44 | name: string;
45 | level?: number;
46 | }
--------------------------------------------------------------------------------
/src/types/score.d.ts:
--------------------------------------------------------------------------------
1 | export interface MaimaiScoreProps {
2 | id: number;
3 | song_name: string;
4 | level: string;
5 | level_index: number;
6 | achievements: number;
7 | fc: string;
8 | fs: string;
9 | dx_score: number;
10 | dx_rating: number;
11 | rate: string;
12 | type: string;
13 | play_time?: string;
14 | upload_time: string;
15 | last_played_time?: string;
16 | }
17 |
18 | export interface ChunithmScoreProps {
19 | id: number;
20 | song_name: string;
21 | level: string;
22 | level_index: number;
23 | score: number;
24 | rating: number;
25 | over_power: number;
26 | clear: string;
27 | full_combo: string;
28 | full_chain: string;
29 | rank: string;
30 | play_time?: string;
31 | upload_time: string;
32 | last_played_time?: string;
33 | }
34 |
35 | export interface MaimaiBestsProps {
36 | standard: MaimaiScoreProps[];
37 | dx: MaimaiScoreProps[];
38 | standard_total: number;
39 | dx_total: number;
40 | }
41 |
42 | export interface ChunithmBestsProps {
43 | bests: ChunithmScoreProps[];
44 | selections: ChunithmScoreProps[];
45 | recents: ChunithmScoreProps[];
46 | }
--------------------------------------------------------------------------------
/src/types/user.d.ts:
--------------------------------------------------------------------------------
1 | export interface UserProps {
2 | id: number;
3 | name: string;
4 | email: string;
5 | permission: number;
6 | register_time: string;
7 | bind: UserBindProps;
8 | token?: string;
9 | // extra
10 | deleted?: boolean;
11 | }
12 |
13 | export interface UserBindProps {
14 | qq?: number;
15 | }
16 |
17 | export interface ConfigProps {
18 | allow_crawl_scores?: boolean;
19 | allow_crawl_name_plate?: boolean;
20 | allow_crawl_frame?: boolean;
21 | allow_crawl_map_icon?: boolean;
22 | crawl_scores_method?: string;
23 | crawl_scores_difficulty?: string[];
24 | allow_third_party_fetch_player?: boolean;
25 | allow_third_party_fetch_scores?: boolean;
26 | allow_third_party_write_data?: boolean;
27 | }
--------------------------------------------------------------------------------
/src/utils/api/alias.ts:
--------------------------------------------------------------------------------
1 | import { fetchAPI } from "./api.ts";
2 |
3 | export async function createAlias(game: string, data: any) {
4 | return fetchAPI(`user/${game}/alias`, { method: "POST", body: data });
5 | }
6 |
7 | export async function voteAlias(game: string, aliasId: number, vote: boolean) {
8 | return fetchAPI(`user/${game}/alias/${aliasId}/vote/${vote ? 'up' : 'down'}`, { method: "POST" });
9 | }
10 |
11 | export async function deleteUserAlias(game: string, aliasId: number) {
12 | return fetchAPI(`user/${game}/alias/${aliasId}`, { method: "DELETE" });
13 | }
14 |
15 | export async function deleteAlias(game: string, aliasId: number) {
16 | return fetchAPI(`user/admin/${game}/alias/${aliasId}`, { method: "DELETE" });
17 | }
18 |
19 | export async function approveAlias(game: string, aliasId: number) {
20 | return fetchAPI(`user/admin/${game}/alias/${aliasId}/approve`, { method: "POST" });
21 | }
22 |
23 | export class AliasList {
24 | game: string = "";
25 | aliases: any[] = [];
26 | searchMap: any = {};
27 |
28 | constructor(game: string) {
29 | this.game = game;
30 | }
31 |
32 | private parseSearchMap() {
33 | this.aliases.forEach((alias) => {
34 | alias.aliases.forEach((aliasText: string) => {
35 | this.searchMap[aliasText] = this.searchMap[aliasText] || [];
36 | this.searchMap[aliasText].push(alias.song_id);
37 | })
38 | })
39 | }
40 |
41 | async fetch() {
42 | const res = await fetchAPI(`${this.game}/alias/list`, { method: "GET" });
43 | const data = await res?.json();
44 | this.aliases = data.aliases;
45 | this.parseSearchMap();
46 |
47 | return this.aliases;
48 | }
49 | }
--------------------------------------------------------------------------------
/src/utils/api/api.ts:
--------------------------------------------------------------------------------
1 | import { isTokenExpired, isTokenUndefined } from "@/utils/session.ts";
2 | import { mutate } from "swr";
3 |
4 | export const API_URL = import.meta.env.VITE_API_URL;
5 |
6 | export async function fetchAPI(endpoint: string, options: { method: string, body?: any, headers?: any }) {
7 | if (!isTokenUndefined() && isTokenExpired()) {
8 | if (endpoint !== "user/refresh") {
9 | await mutate("user/refresh");
10 | }
11 | }
12 |
13 | const { method = "GET", body, headers } = options;
14 |
15 | return await fetch(`${API_URL}/${endpoint}`, {
16 | method,
17 | credentials: "include",
18 | headers: {
19 | "Authorization": `Bearer ${localStorage.getItem("token")}`,
20 | "Content-Type": "application/json",
21 | ...headers,
22 | },
23 | body: body ? JSON.stringify(body) : undefined,
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/api/comment.ts:
--------------------------------------------------------------------------------
1 | import { fetchAPI } from "./api.ts";
2 |
3 | export async function getCommentList(game: string, params: URLSearchParams) {
4 | return fetchAPI(`user/${game}/comment/list?${params.toString()}`, { method: "GET" });
5 | }
6 |
7 | export async function createComment(game: string, data: any) {
8 | return fetchAPI(`user/${game}/comment`, { method: "POST", body: data });
9 | }
10 |
11 | export async function deleteComment(game: string, commentId: number) {
12 | return fetchAPI(`user/${game}/comment/${commentId}`, { method: "DELETE" });
13 | }
14 |
15 | export async function likeComment(game: string, commentId: number) {
16 | return fetchAPI(`user/${game}/comment/${commentId}/like`, { method: "POST" });
17 | }
18 |
19 | export async function unlikeComment(game: string, commentId: number) {
20 | return fetchAPI(`user/${game}/comment/${commentId}/like`, { method: "DELETE" });
21 | }
--------------------------------------------------------------------------------
/src/utils/api/developer.ts:
--------------------------------------------------------------------------------
1 | import { fetchAPI } from "./api.ts";
2 |
3 | export async function sendDeveloperApply(data: any) {
4 | return fetchAPI(`user/developer/apply`, { method: "POST", body: data });
5 | }
6 |
7 | export async function getDeveloperApply() {
8 | return fetchAPI("user/developer/apply", { method: "GET" });
9 | }
10 |
11 | export async function resetDeveloperApiKey() {
12 | return fetchAPI("user/developer/reset", { method: "POST" });
13 | }
14 |
15 | export async function getDevelopers() {
16 | return fetchAPI("user/admin/developers", { method: "GET" });
17 | }
18 |
19 | export async function revokeDeveloper(data: any) {
20 | return fetchAPI("user/admin/developer", { method: "DELETE", body: data });
21 | }
--------------------------------------------------------------------------------
/src/utils/api/misc.ts:
--------------------------------------------------------------------------------
1 | import { fetchAPI } from "@/utils/api/api.ts";
2 | import { Game } from "@/types/game";
3 |
4 | export async function getCrawlStatistic(game: Game) {
5 | return fetchAPI(`${game}/crawl/statistic`, { method: "GET" });
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/api/player.ts:
--------------------------------------------------------------------------------
1 | import { fetchAPI } from "./api.ts";
2 | import { ChunithmPlayerProps, MaimaiPlayerProps } from "@/types/player";
3 |
4 | export async function getPlayerRatingTrend(game: string, version: number) {
5 | return fetchAPI(`user/${game}/player/trend?version=${version}`, { method: "GET" });
6 | }
7 |
8 | export async function unbindPlayer(game: string) {
9 | return fetchAPI(`user/${game}/player`, { method: "DELETE" });
10 | }
11 |
12 | export async function deletePlayerScores(game: string) {
13 | return fetchAPI(`user/${game}/player/scores`, { method: "DELETE" });
14 | }
15 |
16 | export async function createPlayerScores(game: string, scores: any) {
17 | return fetchAPI(`user/${game}/player/scores`, { method: "POST", body: { scores } });
18 | }
19 |
20 | export async function getPlayerPlateById(game: string, id: number) {
21 | return fetchAPI(`user/${game}/player/plate/${id}`, { method: "GET" });
22 | }
23 |
24 | export async function getPlateList(game: string, required: boolean) {
25 | return fetchAPI(`${game}/plate/list?required=${required}`, { method: "GET" });
26 | }
27 |
28 | export async function getPlateById(game: string, id: number) {
29 | return fetchAPI(`${game}/plate/${id}`, { method: "GET" });
30 | }
31 |
32 | export function isMaimaiPlayerProps(obj: unknown): obj is MaimaiPlayerProps {
33 | if (!obj) return false;
34 | return typeof obj === 'object' && 'course_rank' in obj;
35 | }
36 |
37 | export function isChunithmPlayerProps(obj: unknown): obj is ChunithmPlayerProps {
38 | if (!obj) return false;
39 | return typeof obj === 'object' && 'over_power' in obj;
40 | }
--------------------------------------------------------------------------------
/src/utils/api/song/song.tsx:
--------------------------------------------------------------------------------
1 | import { fetchAPI } from "@/utils/api/api.ts";
2 | import { Game } from "@/types/game";
3 |
4 | export async function getSong(game: Game, id: number, version: number) {
5 | const res = await fetchAPI(`${game}/song/${id}?version=${version}`, {
6 | method: "GET",
7 | });
8 | if (res.status === 404) return null;
9 | return await res.json();
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/api/user.ts:
--------------------------------------------------------------------------------
1 | import { fetchAPI } from "./api.ts";
2 |
3 | export async function updateUserProfile(data: unknown) {
4 | return fetchAPI("user/profile", { method: "POST", body: data });
5 | }
6 |
7 | export async function getUserCrawlToken() {
8 | return fetchAPI("user/crawl/token", { method: "GET" });
9 | }
10 |
11 | export async function getCrawlStatus() {
12 | return fetchAPI("user/crawl/status", { method: "GET" });
13 | }
14 |
15 | export async function updateUserConfig(game: string, data: unknown) {
16 | return fetchAPI(`user/${game}/config`, { method: "POST", body: data });
17 | }
18 |
19 | export async function updateUserBind(data: unknown) {
20 | return fetchAPI("user/bind", { method: "POST", body: data });
21 | }
22 |
23 | export async function generateUserToken() {
24 | return fetchAPI("user/token", { method: "POST" });
25 | }
26 |
27 | export async function logoutUser() {
28 | return fetchAPI("user/logout", { method: "POST" });
29 | }
30 |
31 | export async function deleteSelfUser() {
32 | return fetchAPI("user", { method: "DELETE" });
33 | }
34 |
35 | export async function getUsers() {
36 | return fetchAPI("user/admin/users", { method: "GET" });
37 | }
38 |
39 | export async function deleteUsers(data: unknown) {
40 | return fetchAPI("user/admin/users", { method: "DELETE", body: data });
41 | }
42 |
43 | export async function sendBatchEmail(data: unknown) {
44 | return fetchAPI("user/admin/email", { method: "POST", body: data });
45 | }
46 |
47 | export async function updateUser(userId: number, data: unknown) {
48 | return fetchAPI(`user/admin/user/${userId}`, { method: "POST", body: data });
49 | }
50 |
51 | export async function deleteUser(userId: number) {
52 | return fetchAPI(`user/admin/user/${userId}`, { method: "DELETE" });
53 | }
--------------------------------------------------------------------------------
/src/utils/checkProxy.ts:
--------------------------------------------------------------------------------
1 | export const checkProxy = async () => {
2 | const result = {
3 | proxyAvailable: false,
4 | networkError: false,
5 | };
6 | try {
7 | await fetch(`https://maimai.wahlap.com/maimai-mobile/error/`, { mode: 'no-cors' });
8 | } catch (err) {
9 | try {
10 | await fetch(window.location.href, { mode: 'no-cors' });
11 | result.proxyAvailable = true;
12 | } catch (err) {
13 | result.networkError = true;
14 | }
15 | }
16 | return result;
17 | }
--------------------------------------------------------------------------------
/src/utils/context.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | export default createContext({} as any);
--------------------------------------------------------------------------------
/src/utils/modal.tsx:
--------------------------------------------------------------------------------
1 | import { modals } from "@mantine/modals";
2 | import { Text } from "@mantine/core";
3 | import { ReactNode } from "react";
4 |
5 | export const openAlertModal = (title: string, content: string | ReactNode, { ...props }: any = {}) => {
6 | modals.openConfirmModal({
7 | title,
8 | centered: true,
9 | withCloseButton: false,
10 | children: (
11 |
12 | {content}
13 |
14 | ),
15 | labels: { confirm: '确定', cancel: '取消' },
16 | onConfirm: () => modals.closeAll(),
17 | ...props,
18 | });
19 | }
20 |
21 | export const openConfirmModal = (title: string, content: string | ReactNode, onConfirm: () => void, { ...props }: any = {}) => {
22 | modals.openConfirmModal({
23 | title,
24 | centered: true,
25 | withCloseButton: false,
26 | children: (
27 |
28 | {content}
29 |
30 | ),
31 | labels: { confirm: '确定', cancel: '取消' },
32 | onCancel: () => modals.closeAll(),
33 | onConfirm: onConfirm,
34 | ...props,
35 | });
36 | }
37 |
38 | export const openRetryModal = (title: string, content: string | ReactNode, onConfirm: () => void, { ...props }: any = {}) => {
39 | modals.openConfirmModal({
40 | title,
41 | centered: true,
42 | withCloseButton: false,
43 | children: (
44 |
45 | {content}
46 |
47 | ),
48 | labels: { confirm: '重试', cancel: '取消' },
49 | onCancel: () => modals.closeAll(),
50 | onConfirm: onConfirm,
51 | ...props,
52 | });
53 | }
--------------------------------------------------------------------------------
/src/utils/reCaptcha.ts:
--------------------------------------------------------------------------------
1 | class ReCaptcha {
2 | private readonly siteKey: string;
3 | private action: string;
4 |
5 | constructor(siteKey: string, action: string) {
6 | this.siteKey = siteKey;
7 | this.action = action;
8 | }
9 |
10 | render() {
11 | const scriptId = 'recaptcha-script';
12 |
13 | if (!document.getElementById(scriptId)) {
14 | const script = document.createElement('script');
15 | script.id = scriptId;
16 | script.src = `https://recaptcha.google.cn/recaptcha/api.js?render=${this.siteKey}`;
17 | document.body.appendChild(script);
18 | }
19 | }
20 |
21 | async getToken(): Promise {
22 | return new Promise((resolve) => {
23 | window.grecaptcha.ready(() => {
24 | window.grecaptcha.execute(this.siteKey, { action: this.action }).then((token: string) => {
25 | resolve(token);
26 | });
27 | });
28 | });
29 | }
30 |
31 | destroy() {
32 | const scriptId = 'recaptcha-script';
33 | const script = document.getElementById(scriptId);
34 | if (script) {
35 | script.remove();
36 | }
37 |
38 | const iframe = document.querySelector('.grecaptcha-badge');
39 | if (iframe) {
40 | iframe.remove();
41 | }
42 | }
43 | }
44 |
45 | export default ReCaptcha;
--------------------------------------------------------------------------------
/src/utils/session.ts:
--------------------------------------------------------------------------------
1 | import { logoutUser } from "./api/user.ts";
2 |
3 | const getLoginSessionPayload = () => {
4 | const token = localStorage.getItem('token');
5 | if (!token) {
6 | return null;
7 | }
8 |
9 | try {
10 | return JSON.parse(atob(token.split('.')[1]));
11 | } catch (error) {
12 | return null;
13 | }
14 | }
15 |
16 | export const getLoginUserId = () => {
17 | const payload = getLoginSessionPayload();
18 | return payload ? payload.id : null;
19 | }
20 |
21 | export const isTokenExpired = () => {
22 | const token = localStorage.getItem('token');
23 | if (!token) {
24 | return true;
25 | }
26 |
27 | try {
28 | const payload = JSON.parse(atob(token.split('.')[1]));
29 | const currentTime = Date.now();
30 | const expirationTime = payload.exp * 1000;
31 |
32 | if (isNaN(expirationTime)) {
33 | return true;
34 | }
35 |
36 | return currentTime > expirationTime;
37 | } catch (error) {
38 | return true;
39 | }
40 | };
41 |
42 | export const isTokenUndefined = () => {
43 | const token = localStorage.getItem('token');
44 | return !token;
45 | }
46 |
47 | export const isTokenValid = () => {
48 | return !isTokenExpired();
49 | }
50 |
51 | export const logout = () => {
52 | localStorage.removeItem('token');
53 | logoutUser();
54 | }
55 |
56 | export enum UserPermission {
57 | User = 1 << 0,
58 | Developer = 1 << 1,
59 | Administrator = 1 << 2,
60 | }
61 |
62 | export const checkPermission = (permission: UserPermission) => {
63 | const token = localStorage.getItem('token');
64 | if (!token) {
65 | return false;
66 | }
67 |
68 | try {
69 | const payload = JSON.parse(atob(token.split('.')[1]));
70 | return (payload.permission & permission) !== 0;
71 | } catch (error) {
72 | return false;
73 | }
74 | }
75 |
76 | export const permissionToList = (permission: number) => {
77 | const list = [];
78 | if ((permission & UserPermission.User) !== 0) {
79 | list.push(UserPermission.User);
80 | }
81 | if ((permission & UserPermission.Developer) !== 0) {
82 | list.push(UserPermission.Developer);
83 | }
84 | if ((permission & UserPermission.Administrator) !== 0) {
85 | list.push(UserPermission.Administrator);
86 | }
87 | return list;
88 | }
89 |
90 | export const listToPermission = (list: UserPermission[]) => {
91 | let permission = 0;
92 | for (const item of list) {
93 | permission |= item;
94 | }
95 | return permission;
96 | }
--------------------------------------------------------------------------------
/src/utils/validator.ts:
--------------------------------------------------------------------------------
1 | export const validateEmail = (email: string) => {
2 | return /^([a-zA-Z0-9])(([a-zA-Z0-9])*([._-])?([a-zA-Z0-9]))*@(([a-zA-Z0-9\-])+(\.))+([a-zA-Z]{2,4})+$/.test(email);
3 | }
4 |
5 | export const validateUserName = (name: string) => {
6 | return /^[a-zA-Z0-9_]{4,16}$/.test(name);
7 | }
8 |
9 | export const validatePassword = (password: string) => {
10 | return password.length >= 6 || password.length <= 16;
11 | }
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare module "dayjs/locale/*";
3 | declare module "react-color-extractor";
4 | declare module 'react-helmet';
5 | declare module 'wordcloud';
--------------------------------------------------------------------------------
/src/wdyr.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import whyDidYouRender from '@welldone-software/why-did-you-render';
3 |
4 | if (process.env.NODE_ENV === 'development') {
5 | whyDidYouRender(React, {
6 | trackAllPureComponents: true,
7 | });
8 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "node",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "allowSyntheticDefaultImports": true,
23 |
24 | "paths": {
25 | "@/*": ["./src/*"]
26 | }
27 | },
28 | "include": ["src"],
29 | "references": [{ "path": "./tsconfig.node.json" }]
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 | import * as fs from "node:fs";
4 | import path from "node:path";
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [
9 | react(),
10 | {
11 | name: 'generate-version',
12 | closeBundle() {
13 | const version = Date.now().toString();
14 | fs.writeFileSync(path.resolve(__dirname, 'dist/version.json'), JSON.stringify({ version }));
15 | }
16 | }
17 | ],
18 | resolve: {
19 | alias: {
20 | '@': '/src',
21 | }
22 | },
23 | assetsInclude: ['**/*.md'],
24 | build: {
25 | rollupOptions: {
26 | output: {
27 | manualChunks: {
28 | react: ['react', 'react-dom'],
29 | reactRouter: ['react-router', 'react-router-dom'],
30 | mdi: ['@mdi/js'],
31 | },
32 | entryFileNames: `assets/[hash].js`,
33 | chunkFileNames: `assets/[hash].js`,
34 | assetFileNames: `assets/[hash].[ext]`,
35 | }
36 | },
37 | reportCompressedSize: false,
38 | sourcemap: false,
39 | }
40 | })
41 |
--------------------------------------------------------------------------------