Compare commits
9 Commits
b3a697caed
...
71c0450ea3
Author | SHA1 | Date | |
---|---|---|---|
71c0450ea3 | |||
97f66f8a10 | |||
8a864c2cf7 | |||
9f126d84b1 | |||
6d5661c1f3 | |||
63539b22e5 | |||
43ab38db72 | |||
bf653849d6 | |||
42b02fe5bb |
@ -5,6 +5,7 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/scripts/venv" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
7
.idea/misc.xml
Normal file
7
.idea/misc.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12 (actaeon)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (actaeon)" project-jdk-type="Python SDK" />
|
||||
</project>
|
@ -4,6 +4,9 @@ An ARTEMiS frontend.
|
||||
# **WARNING: Back up your database**
|
||||
I am not responsible for misconfigurations that lead to database issues. **You should back up your database before making any changes to it.**
|
||||
|
||||
### **Important**: your default database encoding must be unicode
|
||||
You can change the encoding by running `ALTER DATABASE aime CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`
|
||||
|
||||
## Creating Database Tables
|
||||
Before running the database importer script, you must create the tables in your database. This can be done by running the server once with `AUTOMIGRATE` enabled ([see below](#runtime-variables)), or by running `npm run migrate` (make sure you have run `npm i` first and have the [`DATABASE_URL`](#runtime-variables) environment variable set).
|
||||
|
||||
|
@ -8,7 +8,9 @@ const dbmigrate = DBMigrate.getInstance(true);
|
||||
|
||||
if (process.argv[2] === 'up')
|
||||
dbmigrate.up();
|
||||
else if (process.argv[2] == 'down')
|
||||
else if (process.argv[2] === 'down')
|
||||
dbmigrate.down();
|
||||
else if (process.argv[2] === 'reset')
|
||||
dbmigrate.reset();
|
||||
else
|
||||
console.error('Unknown action', argv[2]);
|
||||
|
53
migrations/20240413060445-create-chuni-static-music-ext.js
Normal file
53
migrations/20240413060445-create-chuni-static-music-ext.js
Normal file
@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
var dbm;
|
||||
var type;
|
||||
var seed;
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var Promise;
|
||||
|
||||
/**
|
||||
* We receive the dbmigrate dependency from dbmigrate initially.
|
||||
* This enables us to not have to rely on NODE_PATH.
|
||||
*/
|
||||
exports.setup = function(options, seedLink) {
|
||||
dbm = options.dbmigrate;
|
||||
type = dbm.dataType;
|
||||
seed = seedLink;
|
||||
Promise = options.Promise;
|
||||
};
|
||||
|
||||
exports.up = function(db) {
|
||||
var filePath = path.join(__dirname, 'sqls', '20240413060445-create-chuni-static-music-ext-up.sql');
|
||||
return new Promise( function( resolve, reject ) {
|
||||
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
|
||||
if (err) return reject(err);
|
||||
console.log('received data: ' + data);
|
||||
|
||||
resolve(data);
|
||||
});
|
||||
})
|
||||
.then(function(data) {
|
||||
return db.runSql(data);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db) {
|
||||
var filePath = path.join(__dirname, 'sqls', '20240413060445-create-chuni-static-music-ext-down.sql');
|
||||
return new Promise( function( resolve, reject ) {
|
||||
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
|
||||
if (err) return reject(err);
|
||||
console.log('received data: ' + data);
|
||||
|
||||
resolve(data);
|
||||
});
|
||||
})
|
||||
.then(function(data) {
|
||||
return db.runSql(data);
|
||||
});
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
"version": 1
|
||||
};
|
@ -0,0 +1 @@
|
||||
DROP TABLE actaeon_chuni_static_music_ext;
|
@ -0,0 +1,14 @@
|
||||
CREATE TABLE actaeon_chuni_static_music_ext (
|
||||
songId INT NOT NULL,
|
||||
chartId INT NOT NULL,
|
||||
|
||||
chartDesigner VARCHAR(255),
|
||||
tapJudgeCount INT NOT NULL,
|
||||
holdJudgeCount INT NOT NULL,
|
||||
slideJudgeCount INT NOT NULL,
|
||||
airJudgeCount INT NOT NULL,
|
||||
flickJudgeCount INT NOT NULL,
|
||||
allJudgeCount INT NOT NULL,
|
||||
|
||||
PRIMARY KEY (songId, chartId)
|
||||
);
|
@ -12,6 +12,7 @@
|
||||
"migrate:up": "dotenvx run -f .env.local -- node db-migrate.cjs up",
|
||||
"migrate": "npm run migrate:up",
|
||||
"migrate:down": "dotenvx run -f .env.local -- node db-migrate.cjs down",
|
||||
"migrate:reset": "dotenvx run -f .env.local -- node db-migrate.cjs reset",
|
||||
"migrate:create": "dotenvx run -f .env.local -- db-migrate create",
|
||||
"db:export": "dotenvx run -f .env.local -- kysely-codegen --exclude-pattern cozynet* --exclude-pattern alembic_version --exclude-pattern actaeon_migrations --out-file src/types/db.d.ts && node process-db.js"
|
||||
},
|
||||
|
@ -52,6 +52,9 @@ Usage: `py db-importer.py [options] <game> [game_options]`
|
||||
|
||||
The database importer requires the `DATABASE_URL` environment variable to be set, which can be set in an `.env` file in the format `DATABASE_URL=mysql://user:pass@host:port/db_name` (see the `--env` option)
|
||||
|
||||
### **Important**: your default database encoding must be unicode
|
||||
You can change the encoding by running `ALTER DATABASE aime CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`
|
||||
|
||||
# **WARNING: Back up your database**
|
||||
I am not responsible for misconfigurations that lead to database issues. **You should back up your database before making any changes to it.**
|
||||
|
||||
|
@ -63,11 +63,36 @@ class Chuni(Importer):
|
||||
self.get_xml('trophy', 'Trophy', ('./name/id', int), './name/str', ('./rareType', int), './explainText')
|
||||
)
|
||||
|
||||
def import_charts(self):
|
||||
inserts = []
|
||||
for file in chain(self.data_dir.glob(f'A000/music/*/*.c2s'),
|
||||
self.opt_dir.glob(f'*/music/*/*.c2s')):
|
||||
print(file)
|
||||
data = {}
|
||||
song, chart = map(int, file.stem.split('_'))
|
||||
if song >= 8000: chart = 5
|
||||
with open(file, 'r', encoding='utf8') as f:
|
||||
for line in f.readlines():
|
||||
parts = line.strip().split('\t')
|
||||
if len(parts) == 2:
|
||||
data[parts[0]] = parts[1]
|
||||
inserts.append((song, chart, data['CREATOR'], data['T_JUDGE_TAP'], data['T_JUDGE_HLD'], data['T_JUDGE_SLD'],
|
||||
data['T_JUDGE_AIR'], data['T_JUDGE_FLK'], data['T_JUDGE_ALL']))
|
||||
fields = ['songId', 'chartId', 'chartDesigner', 'tapJudgeCount', 'holdJudgeCount', 'slideJudgeCount',
|
||||
'airJudgeCount', 'flickJudgeCount', 'allJudgeCount']
|
||||
self.cur.executemany(
|
||||
f'''INSERT INTO actaeon_chuni_static_music_ext({','.join(fields)})
|
||||
VALUES ({','.join(['%s'] * len(fields))})
|
||||
ON DUPLICATE KEY UPDATE {','.join(f"{f}={f}" for f in fields)}''',
|
||||
inserts
|
||||
)
|
||||
|
||||
def do_import(self):
|
||||
self.import_map_icon()
|
||||
self.import_name_plate()
|
||||
self.import_system_voice()
|
||||
self.import_trophies()
|
||||
self.import_charts()
|
||||
|
||||
@staticmethod
|
||||
def register(parser):
|
||||
|
@ -22,11 +22,14 @@ export const getMusic = async (musicId?: number) => {
|
||||
.on('favorite.favKind', '=', 1)
|
||||
.on('favorite.user', '=', user?.id!)
|
||||
)
|
||||
.innerJoin('actaeon_chuni_static_music_ext as musicExt', join =>
|
||||
join.onRef('music.songId', '=', 'musicExt.songId')
|
||||
.onRef('music.chartId', '=', 'musicExt.chartId'))
|
||||
.select(({ fn }) => [...CHUNI_MUSIC_PROPERTIES,
|
||||
'score.isFullCombo', 'score.isAllJustice', 'score.isSuccess', 'score.scoreRank', 'score.scoreMax',
|
||||
'score.maxComboCount',
|
||||
fn<boolean>('NOT ISNULL', ['favorite.favId']).as('favorite'),
|
||||
chuniRating()])
|
||||
chuniRating()] as const)
|
||||
.where(({ selectFrom, eb, and, or }) => and([
|
||||
eb('music.version', '=', selectFrom('chuni_static_music')
|
||||
.select(({ fn }) => fn.max('version').as('latest'))),
|
||||
|
@ -36,6 +36,9 @@ export async function getPlaylog(opts: GetPlaylogOptions) {
|
||||
.innerJoin('chuni_static_music as music', join => join
|
||||
.onRef('music.songId', '=', 'playlog.musicId')
|
||||
.onRef('music.chartId', '=', 'playlog.level'))
|
||||
.innerJoin('actaeon_chuni_static_music_ext as musicExt', join => join
|
||||
.onRef('music.songId', '=', 'musicExt.songId')
|
||||
.onRef('music.chartId', '=', 'musicExt.chartId'))
|
||||
.where(({ and, eb, selectFrom }) => and([
|
||||
eb('playlog.user', '=', user.id),
|
||||
eb('music.version', '=', selectFrom('chuni_static_music')
|
||||
@ -45,14 +48,14 @@ export async function getPlaylog(opts: GetPlaylogOptions) {
|
||||
({ ref }) => ['playlog.id', 'playlog.sortNumber', 'playlog.playDate', 'playlog.userPlayDate', 'playlog.track',
|
||||
'playlog.score', 'playlog.rank', 'playlog.maxCombo', 'playlog.maxChain', 'playlog.rateTap',
|
||||
'playlog.rateHold', 'playlog.rateSlide', 'playlog.rateAir', 'playlog.rateFlick', 'playlog.judgeGuilty',
|
||||
'playlog.judgeAttack', 'playlog.judgeCritical', 'playlog.judgeHeaven', 'playlog.playerRating',
|
||||
'playlog.judgeAttack', 'playlog.judgeCritical', 'playlog.judgeHeaven', 'playlog.playerRating', 'playlog.judgeJustice',
|
||||
'playlog.isNewRecord', 'playlog.isFullCombo', 'playlog.fullChainKind', 'playlog.isAllJustice',
|
||||
'playlog.playKind', 'playlog.isClear', 'playlog.placeName',
|
||||
...CHUNI_MUSIC_PROPERTIES,
|
||||
chuniRating(ref('playlog.score')),
|
||||
sql<number>`(playlog.playerRating - (LEAD(playlog.playerRating) OVER (ORDER BY id DESC)))`
|
||||
.as('playerRatingChange')
|
||||
])
|
||||
] as const)
|
||||
.orderBy('playlog.id desc')
|
||||
)
|
||||
.selectFrom('p')
|
||||
|
@ -93,6 +93,9 @@ export async function getUserRating(user: UserPayload) {
|
||||
))`.as('score'), join => join.onTrue())
|
||||
.innerJoin('chuni_static_music as music', join => join.onRef('score.musicId', '=', 'music.songId')
|
||||
.onRef('score.level', '=', 'music.chartId'))
|
||||
.innerJoin('actaeon_chuni_static_music_ext as musicExt', join => join
|
||||
.onRef('music.songId', '=', 'musicExt.songId')
|
||||
.onRef('music.chartId', '=', 'musicExt.chartId'))
|
||||
.select(({ lit }) => [...CHUNI_MUSIC_PROPERTIES, chuniRating(sql.raw(`CAST(score.scoreMax AS INT)`)),
|
||||
sql<string>`CAST(score.scoreMax AS INT)`.as('scoreMax'),
|
||||
lit<number>(1).as('pastIndex')
|
||||
@ -104,8 +107,10 @@ export async function getUserRating(user: UserPayload) {
|
||||
const top = await db.selectFrom('chuni_score_best as score')
|
||||
.innerJoin('chuni_static_music as music', join => join
|
||||
.onRef('music.songId', '=', 'score.musicId')
|
||||
.onRef('music.chartId', '=', 'score.level')
|
||||
)
|
||||
.onRef('music.chartId', '=', 'score.level'))
|
||||
.innerJoin('actaeon_chuni_static_music_ext as musicExt', join => join
|
||||
.onRef('music.songId', '=', 'musicExt.songId')
|
||||
.onRef('music.chartId', '=', 'musicExt.chartId'))
|
||||
.where(({ eb, and, selectFrom }) => and([
|
||||
eb('user', '=', user.id),
|
||||
eb('score.level', '!=', 5),
|
||||
|
@ -36,7 +36,7 @@ export default async function ChuniDashboard() {
|
||||
</div>
|
||||
<div className="text-lg font-semibold px-4 pt-4 border-t border-gray-500 md:hidden">Recent Plays</div>
|
||||
<div className="my-4 w-full flex-grow grid gap-2 grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 5xl:grid-cols-6 6xl:grid-cols-8">
|
||||
{playlog.data.map((entry, i) => <ChuniPlaylogCard className="w-full h-48"
|
||||
{playlog.data.map((entry, i) => <ChuniPlaylogCard className="w-full h-52"
|
||||
badgeClass="h-5 lg:h-[1.125rem] xl:h-6 2xl:h-[1.125rem] 4xl:h-6 5xl:h-[1.125rem]"
|
||||
playlog={entry} key={i} />)}
|
||||
</div>
|
||||
|
@ -19,7 +19,8 @@ type ChuniMusicPlaylogProps = {
|
||||
export const ChuniMusicPlaylog = ({ music, playlog }: ChuniMusicPlaylogProps) => {
|
||||
type Music = (typeof music)[number];
|
||||
type Playlog = (typeof playlog)['data'][number];
|
||||
const defaultExpanded: Record<string, Set<string>> = {}
|
||||
|
||||
const [selected, setSelected] = useState(new Set<string>());
|
||||
|
||||
const difficulties: (Music & { playlog: Playlog[] })[] = [];
|
||||
music.forEach(m => {
|
||||
@ -27,65 +28,69 @@ export const ChuniMusicPlaylog = ({ music, playlog }: ChuniMusicPlaylogProps) =>
|
||||
});
|
||||
|
||||
playlog.data.forEach(play => {
|
||||
defaultExpanded[play.chartId!] = new Set();
|
||||
difficulties[play.chartId!].playlog.push(play);
|
||||
});
|
||||
|
||||
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||
|
||||
const badgeClass = 'h-6 sm:h-8';
|
||||
|
||||
return (<div className="flex flex-col w-full px-2 sm:px-0">
|
||||
{difficulties.map((data, i) => {
|
||||
const rank = CHUNI_SCORE_RANKS[data.scoreRank!];
|
||||
const badges = [
|
||||
!!data.scoreRank && <ChuniScoreBadge variant={getVariantFromRank(data.scoreRank)} className={`${badgeClass} tracking-[0.05cqw]`} key="1">
|
||||
{rank.endsWith('+') ? <>
|
||||
{rank.slice(0, -1)}
|
||||
<div className="inline-block translate-y-[-15cqh]">+</div>
|
||||
</> : rank}
|
||||
</ChuniScoreBadge>,
|
||||
data.isSuccess ? <ChuniLampSuccessBadge key="2" className={badgeClass} success={data.isSuccess} /> : null,
|
||||
<ChuniLampComboBadge key="3" className={badgeClass} {...data} />
|
||||
].filter(x => x);
|
||||
return (<div className="flex flex-col w-full px-1 sm:px-0">
|
||||
<Accordion selectionMode="multiple" selectedKeys={selected}>
|
||||
{difficulties.map((data, i) => {
|
||||
const rank = CHUNI_SCORE_RANKS[data.scoreRank!];
|
||||
const badges = [
|
||||
!!data.scoreRank && <ChuniScoreBadge variant={getVariantFromRank(data.scoreRank)} className={`${badgeClass} tracking-[0.05cqw]`} key="1">
|
||||
{rank.endsWith('+') ? <>
|
||||
{rank.slice(0, -1)}
|
||||
<div className="inline-block translate-y-[-15cqh]">+</div>
|
||||
</> : rank}
|
||||
</ChuniScoreBadge>,
|
||||
data.isSuccess ? <ChuniLampSuccessBadge key="2" className={badgeClass} success={data.isSuccess} /> : null,
|
||||
(data.isFullCombo || data.isAllJustice) && <ChuniLampComboBadge key="3" className={badgeClass} {...data} />
|
||||
].filter(x => x);
|
||||
|
||||
const toggleExpanded = () => expanded[i] && setExpanded(e =>
|
||||
({ ...e,
|
||||
[i]: e[i].size ? new Set() : new Set(['1'])
|
||||
}));
|
||||
|
||||
return (<div key={i} className="mb-2 border-b pb-2 border-gray-500 flex flex-row flex-wrap">
|
||||
<div className={`flex items-center gap-2 flex-wrap w-full lg:w-auto lg:flex-grow ${data.playlog.length ? 'cursor-pointer' : ''}`} onClick={toggleExpanded}>
|
||||
<div className="flex items-center">
|
||||
<div className="w-14 mr-2 p-0.5 bg-black">
|
||||
<ChuniLevelBadge className="w-full" music={data} />
|
||||
// <div key={i} className="mb-2 border-b pb-2 border-gray-500 flex flex-row flex-wrap items-center">
|
||||
return (<AccordionItem key={i.toString()} classNames={{ trigger: 'py-0 my-2' }} title={<div className="flex flex-row flex-wrap items-center gap-y-1.5"
|
||||
onClick={() => {
|
||||
const key = i.toString();
|
||||
setSelected(s => s.has(key) ? new Set([...s].filter(k => k !== key)) : new Set([...s, key]))
|
||||
}}>
|
||||
<div className={`flex items-center gap-2 flex-wrap lg:flex-grow ${data.playlog.length ? 'cursor-pointer w-full lg:w-auto' : 'flex-grow'}`}>
|
||||
<div className="flex items-center">
|
||||
<div className="w-14 mr-2 p-0.5 bg-black">
|
||||
<ChuniLevelBadge className="w-full" music={data} />
|
||||
</div>
|
||||
<div className="text-xl font-semibold">{CHUNI_DIFFICULTIES[i]}</div>
|
||||
</div>
|
||||
<div className="text-xl font-semibold">{CHUNI_DIFFICULTIES[i]}</div>
|
||||
{!data.playlog.length && <div className="text-right italic text-gray-500 flex-grow">No Play History</div>}
|
||||
{data.rating ? <ChuniRating className="text-2xl text-right" rating={+data.rating * 100} /> : null}
|
||||
{data.scoreMax ? <div className="ml-2 text-center flex-grow sm:flex-grow-0 max-sm:text-sm">
|
||||
<span className="font-semibold">High Score: </span>{data.scoreMax.toLocaleString()}
|
||||
</div> : null}
|
||||
{data.maxComboCount ? <div className="ml-2 text-center flex-grow sm:flex-grow-0 max-sm:text-sm">
|
||||
<span className="font-semibold">Max Combo: </span>{data.maxComboCount.toLocaleString()}
|
||||
</div> : null}
|
||||
</div>
|
||||
{!data.playlog.length && <div className="text-right italic text-gray-500 flex-grow">No Play History</div>}
|
||||
{data.rating ? <ChuniRating className="text-2xl text-right" rating={+data.rating * 100} /> : null}
|
||||
{data.scoreMax ? <div className="ml-2 text-center flex-grow sm:flex-grow-0">
|
||||
<span className="font-semibold">High Score: </span>{data.scoreMax.toLocaleString()}
|
||||
{badges.length ? <div className={`flex-grow items-center lg:flex-grow-0 ml-auto mr-auto sm:ml-0 lg:ml-auto lg:mr-0 flex gap-0.5 flex-wrap justify-center sm:justify-start ${data.playlog.length ? 'cursor-pointer' : ''}`}>
|
||||
{badges}
|
||||
</div> : null}
|
||||
{data.maxComboCount ? <div className="ml-2 text-center flex-grow sm:flex-grow-0">
|
||||
<span className="font-semibold">Max Combo: </span>{data.maxComboCount.toLocaleString()}
|
||||
</div> : null}
|
||||
</div>
|
||||
{badges.length ? <div className={`flex-grow lg:flex-grow-0 ml-auto mr-auto sm:ml-0 lg:ml-auto lg:mr-0 mt-2 flex gap-0.5 flex-wrap justify-center sm:justify-start ${data.playlog.length ? 'cursor-pointer' : ''}`} onClick={toggleExpanded}>
|
||||
{badges}
|
||||
</div> : null}
|
||||
{data.playlog.length ? <Accordion selectedKeys={expanded[i]} onSelectionChange={k => setExpanded(e => ({ ...e, [i]: k as any }))}>
|
||||
<AccordionItem key="1" title="Play History">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 4xl:grid-cols-4 5xl:grid-cols-5 6xl:grid-cols-6 gap-2">
|
||||
{data.playlog.map(p => <ChuniPlaylogCard key={p.id}
|
||||
showDetails
|
||||
badgeClass="h-5 sm:h-6 md:h-5 lg:h-[1.125rem] 3xl:h-5"
|
||||
playlog={p} className="h-64 md:h-52" />)}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
</Accordion> : null
|
||||
}
|
||||
</div>)
|
||||
})}
|
||||
</div>}>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2 mb-3 justify-center sm:justify-end max-sm:text-xs">
|
||||
<span className="mr-auto max-sm:w-full text-center"><span className="font-semibold">Chart designer:</span> {data.chartDesigner}</span>
|
||||
{!!data.tapJudgeCount && <span><span className="font-semibold">Tap:</span> {data.tapJudgeCount}</span>}
|
||||
{!!data.flickJudgeCount && <span><span className="font-semibold">Flick:</span> {data.flickJudgeCount}</span>}
|
||||
{!!data.holdJudgeCount && <span><span className="font-semibold">Hold:</span> {data.holdJudgeCount}</span>}
|
||||
{!!data.slideJudgeCount && <span><span className="font-semibold">Slide:</span> {data.slideJudgeCount}</span>}
|
||||
{!!data.airJudgeCount && <span><span className="font-semibold">Air:</span> {data.airJudgeCount}</span>}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 4xl:grid-cols-4 5xl:grid-cols-5 6xl:grid-cols-6 gap-2">
|
||||
{data.playlog.map(p => <ChuniPlaylogCard key={p.id}
|
||||
showDetails
|
||||
badgeClass="h-5 sm:h-6 md:h-5 lg:h-[1.125rem] 3xl:h-5 md:-mt-1"
|
||||
playlog={p} className="h-64 md:h-52" />)}
|
||||
</div>
|
||||
</AccordionItem>);
|
||||
})}
|
||||
</Accordion>
|
||||
</div>);
|
||||
};
|
||||
|
@ -52,8 +52,11 @@ export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
||||
const [userDropdownOpen, setUserDropdownOpen] = useState(false);
|
||||
const [menuTranslate, setMenuTranslate] = useState<number | null>(null);
|
||||
const [notificationsTranslate, setNotificationsTranslate] = useState<number | null>(null);
|
||||
const [navbarShowing, setNavbarShowing] = useState(true);
|
||||
const reloaded = useReloaded();
|
||||
const menusOpened = useRef(false);
|
||||
const scrollOffset = useRef(0);
|
||||
const lastScroll = useRef<number | null>(null);
|
||||
|
||||
const path = pathname === '/' ? (user?.homepage ?? '/dashboard') : pathname;
|
||||
|
||||
@ -71,8 +74,23 @@ export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
||||
router.replace('', { scroll: false });
|
||||
}, [router, reloaded]);
|
||||
|
||||
useWindowListener('scroll', () => {
|
||||
if (lastScroll.current === null) lastScroll.current = window.scrollY;
|
||||
scrollOffset.current += window.scrollY - lastScroll.current;
|
||||
lastScroll.current = window.scrollY;
|
||||
|
||||
if (scrollOffset.current < -100 || window.scrollY < 35)
|
||||
setNavbarShowing(true);
|
||||
else if (scrollOffset.current > 100)
|
||||
setNavbarShowing(false);
|
||||
});
|
||||
|
||||
useWindowListener('scrollend', () => {
|
||||
scrollOffset.current = 0;
|
||||
});
|
||||
|
||||
const { ref } = useSwipeable({
|
||||
touchEventOptions: { passive: false },
|
||||
touchEventOptions: { passive: true },
|
||||
onSwiped: e => {
|
||||
const speedX = Math.abs(e.vxvy[0]);
|
||||
const speedY = Math.abs(e.vxvy[1]);
|
||||
@ -90,6 +108,7 @@ export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
||||
|
||||
setMenuTranslate(null);
|
||||
setNotificationsTranslate(null);
|
||||
if (!isMenuOpen && !isNotificationsOpen)
|
||||
document.body.classList.remove('touch-none', 'overflow-hidden');
|
||||
},
|
||||
onSwipeStart: e => {
|
||||
@ -109,21 +128,17 @@ export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
||||
|
||||
if ((isMenuOpen && e.dir === 'Left') || (allMenusClosed && e.dir === 'Right' && xPercent <= 0.6)) {
|
||||
setMenuTranslate(e.deltaX);
|
||||
e.event.preventDefault();
|
||||
document.body.classList.add('touch-none', 'overflow-hidden');
|
||||
} else if ((isNotificationsOpen && e.dir === 'Right' || (allMenusClosed && e.dir === 'Left' && xPercent >= 0.4))) {
|
||||
setNotificationsTranslate(e.deltaX);
|
||||
e.event.preventDefault();
|
||||
document.body.classList.add('touch-none', 'overflow-hidden');
|
||||
}
|
||||
},
|
||||
onSwiping: e => {
|
||||
if (menuTranslate !== null) {
|
||||
setMenuTranslate(e.deltaX);
|
||||
e.event.preventDefault();
|
||||
} else if (notificationsTranslate !== null) {
|
||||
setNotificationsTranslate(e.deltaX);
|
||||
e.event.preventDefault();
|
||||
}
|
||||
}
|
||||
}) as { ref: RefCallback<Document>; };
|
||||
@ -133,6 +148,13 @@ export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
||||
return () => ref({} as any);
|
||||
}, [ref]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMenuOpen || isNotificationsOpen)
|
||||
document.body.classList.add('touch-none', 'overflow-hidden');
|
||||
else
|
||||
document.body.classList.remove('touch-none', 'overflow-hidden');
|
||||
}, [isMenuOpen, isNotificationsOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (user)
|
||||
getFriendRequests().then(setFriendRequests);
|
||||
@ -236,7 +258,8 @@ export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
||||
}, [friendRequests]);
|
||||
|
||||
const topNavbar = useMemo(() => {
|
||||
return (<Navbar className="w-full fixed" classNames={{ wrapper: 'max-w-full p-0' }} shouldHideOnScroll={breakpoint === undefined} height="5.5rem">
|
||||
return (<Navbar className={`w-lvw fixed transition ${!navbarShowing && breakpoint === undefined ? '-translate-y-full' : ''}`}
|
||||
classNames={{ wrapper: 'max-w-full p-0' }} height="5.5rem">
|
||||
<div className="flex h-header px-6 items-center flex-shrink-0 w-full z-[48]">
|
||||
<Button className="text-2xl font-bold cursor-pointer flex items-center m-0 ps-1.5 pe-2 mr-6" variant="light"
|
||||
onClick={() => setMenuOpen(true)}>
|
||||
@ -323,7 +346,7 @@ export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
||||
}
|
||||
</div>
|
||||
</Navbar>);
|
||||
}, [breakpoint, routeGroup, friendRequests, userDropdownOpen, isNotificationsOpen, user, notifications]);
|
||||
}, [breakpoint, routeGroup, friendRequests, userDropdownOpen, isNotificationsOpen, user, notifications, navbarShowing]);
|
||||
|
||||
const leftSidebar = useMemo(() => {
|
||||
return (<div className={`fixed inset-0 w-full h-full z-[49] ${isMenuOpen ? '' : 'pointer-events-none'}`}>
|
||||
@ -472,10 +495,10 @@ export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
||||
{ leftSidebar }
|
||||
{ rightSidebar }
|
||||
{/* begin top navbar */}
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div className="flex flex-col flex-grow h-full">
|
||||
{topNavbar}
|
||||
|
||||
<div className={`sm:px-5 flex-grow pt-fixed flex flex-col`}>
|
||||
<div className={`sm:px-5 flex-grow pt-fixed flex flex-col h-full`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,6 +16,10 @@ $scrollbar-size: 13px;
|
||||
|
||||
// dont theme scrollbars on mobile
|
||||
@media (hover: hover) {
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
@supports not selector(::-webkit-scrollbar) {
|
||||
* {
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-bg);
|
||||
|
@ -1,29 +1,4 @@
|
||||
export const ChuniPenguinIcon = ({ className }: { className?: string; }) => <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 575.97 1066.56" enable-background="new 0 0 575.97 1066.56" xmlSpace="preserve" fill="currentColor" className={className}>
|
||||
<path d="M221.68,0c26.2,53.56,47.17,108.69,50.4,170.46c12.03-22.99,25.9-43.3,35.81-65.84c0.55,0.09,1.11,0.18,1.66,0.28
|
||||
c0.39,4.67,1.15,9.34,1.11,14c-0.27,36.51-8.89,71.22-23.11,104.65c-2.73,6.42-6.06,12.61-9.31,18.79
|
||||
c-1.49,2.84-2.04,4.27,2.24,4.4c33.79,1.05,67.46,4.01,101.1,7.11c40.28,3.7,78.14,35.91,88.71,74.85
|
||||
c2.54,9.34,4.83,18.73,5.25,28.42c0.23,5.27,1.53,11.24-5.11,14.24c-0.61,0.28-0.95,1.33-1.25,2.1
|
||||
c-3.98,10.33-9.62,19.78-15.81,28.87c-6.47,9.5-10.78,19.85-13.62,30.92c-3.42,13.36-6.97,26.7-10.59,40.53
|
||||
c2.16-0.73,2.81-2.31,3.87-3.43c34.4-36.21,73.61-65.43,120.82-82.99c4.54-1.69,9.22-2.89,14.14-2.74
|
||||
c5.97,0.19,8.93,3.61,7.7,9.49c-0.6,2.9-1.8,5.72-3.08,8.41c-14.9,31.17-36.12,57.28-62.57,79.48
|
||||
c-25.66,21.54-51.57,42.74-79.05,61.94c-3.19,2.23-3.79,5.27-4.02,8.64c-0.49,7.38,1,14.6,1.95,21.85
|
||||
c1.73,13.23,2.04,26.56,2.69,39.85c0.81,16.63,1.37,33.31,1.18,49.95c-0.29,25.61-5.24,50.02-21.36,70.95
|
||||
c-2.08,2.7-1.63,5.67-1.3,8.62c3.51,31.74,1.61,63.21-5.37,94.31c-6.59,29.32-22.61,53.87-40.63,77.22
|
||||
c-13.54,17.55-30.65,30.92-50.2,41.25c-6.99,3.69-14.4,6.31-22.21,7.25c-4.29,0.52-4.01,2.26-3.29,5.55
|
||||
c4.14,18.97,14.64,34.33,26.47,49.24c10.09,12.72,23.64,19.05,38.27,24.13c5.35,1.86,10.79,3.54,15.91,5.91
|
||||
c6.25,2.9,6.31,7.24,0.59,11.17c-10.79,7.4-23.01,7.22-35.19,6.21c-9.12-0.76-18.07-2.79-26.9-5.35
|
||||
c-8.22-2.39-14.37-7.21-19.56-13.89c-13.28-17.09-19.18-37.09-23.62-57.76c-1.58-7.37-2.06-14.84-2.35-22.32
|
||||
c-0.12-3.18-1.43-4.08-4.42-4.63c-35.69-6.53-70.33-16.92-103.64-31.11c-29.5-12.57-55.91-30.67-79.54-52.14
|
||||
c-33.9-30.81-57.32-68.72-69.26-113.09c-6.75-25.06-5.48-50.85-3.95-76.51c2.07-34.54,10.82-67.64,21.4-100.36
|
||||
c1.76-5.45,1.94-5.55,6.47-2.07c9.12,7,18.19,14.07,27.29,21.09c1.15,0.89,2.23,2.54,3.81,1.92c1.94-0.76,1.26-2.87,1.34-4.44
|
||||
c0.95-18.47,2.63-36.86,6.14-55.04c4.61-23.86,16.43-44.56,28.94-64.89c2.49-4.04,2.43-6.65-0.1-10.61
|
||||
c-28.64-44.79-47.6-93.27-55.22-146.06c-2.81-19.45-4.95-38.96-6.55-58.54c-0.42-5.16-0.64-10.34,1.28-15.31
|
||||
c2.25-5.84,6.39-7.36,11.95-4.51c2.27,1.17,3.99,2.97,5.83,4.67c19.95,18.47,34.4,41.04,47.76,64.38
|
||||
c5.63,9.83,10.85,19.89,16.15,29.9c7.61,14.37,13.01,29.55,16.18,45.51c0.28,1.43,0.72,2.83,1.3,5.09
|
||||
c3.4-7.96,6.01-15.28,8.25-22.75c5.88-19.62,12.46-39.03,18.53-58.59c5.95-19.17,17.46-33.74,33.1-45.71
|
||||
c8.37-6.41,15.5-14.49,25.78-18.2c2.35-0.85,1.84-2.24,0.6-3.84c-35.87-46.59-55.6-100.48-70.69-156.46
|
||||
c-0.22-0.8-0.38-1.62-0.48-2.44c-0.03-0.28,0.2-0.59,0.53-1.46c22.8,14.47,39.21,34.72,54.35,57.82
|
||||
c-1.89-15.06-3.3-28.96-3.98-42.97c-0.96-19.68,1.65-38.87,5.8-57.94c3.5-16.11,7.73-32.04,13.71-47.45
|
||||
C221.02,0,221.35,0,221.68,0z"/>
|
||||
export const ChuniPenguinIcon = ({ className }: { className?: string; }) => <svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 576 1066.6" fill="currentColor" className={className}>
|
||||
<path d="M222 0c26 54 47 109 50 171 12-23 26-44 36-66 9 44-9 97-30 137 70 15 195 0 198 115-13 38-42 73-47 117 70-84 233-143 81 8-24 26-64 42-83 71 5 64 16 133-17 191 12 86-27 195-118 220-11 14 14 41 23 55 7 21 68 21 55 41-11 7-23 7-35 6-61 2-64-65-77-104-36-6-71-17-104-31C60 891-14 795 1 689c2-34 11-67 22-100 5-12 24 15 33 19 3 4 6 1 6-3 0-42 10-84 35-120-83-152-92-396 38-85 21-54 27-119 86-145-27-48-61-106-70-164 23 14 39 34 54 57-8-50-4-103 17-148z" />
|
||||
</svg>;
|
||||
|
@ -32,6 +32,14 @@ const getChangeColor = (val: number) => {
|
||||
};
|
||||
|
||||
export const ChuniPlaylogCard = ({ playlog, className, badgeClass, showDetails }: ChuniPlaylogCardProps) => {
|
||||
const rate = showDetails ? <>
|
||||
{!!playlog.tapJudgeCount && <div>Tap: {(playlog.rateTap! / 100).toFixed(2)}%</div>}
|
||||
{!!playlog.flickJudgeCount && <div>Flick: {(playlog.rateFlick! / 100).toFixed(2)}%</div>}
|
||||
{!!playlog.holdJudgeCount && <div>Hold: {(playlog.rateHold! / 100).toFixed(2)}%</div>}
|
||||
{!!playlog.slideJudgeCount && <div>Slide: {(playlog.rateSlide! / 100).toFixed(2)}%</div>}
|
||||
{!!playlog.airJudgeCount && <div>Air: {(playlog.rateAir! / 100).toFixed(2)}%</div>}
|
||||
</> : null;
|
||||
|
||||
return (<TickerHoverProvider>{setHover => <div onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
className={`rounded-md bg-content1 relative flex flex-col p-2 pt-1 border border-black/25 ${className ?? ''}`}>
|
||||
<div className="flex">
|
||||
@ -68,11 +76,8 @@ export const ChuniPlaylogCard = ({ playlog, className, badgeClass, showDetails }
|
||||
|
||||
{showDetails && <div className="hidden md:flex flex-col text-sm gap-1 items-end text-nowrap ml-1">
|
||||
<div className="text-xs my-1">{formatJst(playlog.userPlayDate!)}</div>
|
||||
<div>Tap: {(playlog.rateTap! / 100).toFixed(2)}%</div>
|
||||
<div>Flick: {(playlog.rateFlick! / 100).toFixed(2)}%</div>
|
||||
<div>Hold: {(playlog.rateHold! / 100).toFixed(2)}%</div>
|
||||
<div>Slide: {(playlog.rateSlide! / 100).toFixed(2)}%</div>
|
||||
<div>Air: {(playlog.rateAir! / 100).toFixed(2)}%</div>
|
||||
{rate}
|
||||
<div style={{ marginBottom: `${(+!playlog.tapJudgeCount + +!playlog.flickJudgeCount + +!playlog.holdJudgeCount + +!playlog.slideJudgeCount + +!playlog.airJudgeCount) * 1.25}rem` }} />
|
||||
</div>}
|
||||
</div>
|
||||
<div
|
||||
@ -84,20 +89,17 @@ export const ChuniPlaylogCard = ({ playlog, className, badgeClass, showDetails }
|
||||
<ChuniLampComboBadge {...playlog} />
|
||||
{!!playlog.isNewRecord && <ChuniScoreBadge variant="gold" fontSize="sm">NEW RECORD</ChuniScoreBadge>}
|
||||
</div>
|
||||
<div className="flex flex-wrap text-xs justify-around drop-shadow-sm gap-1">
|
||||
<div className="text-chuni-justice-critical">Justice Critical: {playlog.judgeHeaven}</div>
|
||||
<div className="text-chuni-justice">Justice: {playlog.judgeCritical}</div>
|
||||
<div className="text-chuni-attack">Attack: {playlog.judgeAttack}</div>
|
||||
<div className="text-chuni-miss">Miss: {playlog.judgeGuilty}</div>
|
||||
<div className="flex flex-wrap text-xs justify-around drop-shadow gap-1">
|
||||
<div className="text-yellow-500 dark:text-yellow-400">J. Heaven: {playlog.judgeHeaven}</div>
|
||||
<div className="text-yellow-600 dark:text-yellow-500">J. Critical: {playlog.judgeCritical}</div>
|
||||
<div className="text-orange-500">Justice: {playlog.judgeJustice}</div>
|
||||
<div className="text-emerald-600">Attack: {playlog.judgeAttack}</div>
|
||||
<div className="text-gray-400">Miss: {playlog.judgeGuilty}</div>
|
||||
</div>
|
||||
{showDetails && <>
|
||||
<Divider className="md:hidden my-2"/>
|
||||
<div className="flex flex-wrap text-xs justify-around md:hidden gap-1 mb-1">
|
||||
<div>Tap: {(playlog.rateTap! / 100).toFixed(2)}%</div>
|
||||
<div>Flick: {(playlog.rateFlick! / 100).toFixed(2)}%</div>
|
||||
<div>Hold: {(playlog.rateHold! / 100).toFixed(2)}%</div>
|
||||
<div>Slide: {(playlog.rateSlide! / 100).toFixed(2)}%</div>
|
||||
<div>Air: {(playlog.rateAir! / 100).toFixed(2)}%</div>
|
||||
{rate}
|
||||
</div>
|
||||
</>}
|
||||
</div>}
|
||||
|
@ -492,7 +492,6 @@ const payloadValid = (payload: any, filterers: Filterers<any, any>) => {
|
||||
for (const filterer of filterers) {
|
||||
if (!(filterer.name in payload)) continue;
|
||||
const data = payload[filterer.name];
|
||||
console.log(filterer.name, data)
|
||||
if (filterer.type === 'select' && !(data instanceof Set))
|
||||
return false;
|
||||
if (filterer.type === 'slider' && !Array.isArray(data))
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReactNode, useRef } from 'react';
|
||||
import { ReactNode, useEffect, useRef } from 'react';
|
||||
import { useWindowVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useResizeObserver } from 'usehooks-ts';
|
||||
|
||||
@ -27,6 +27,10 @@ export const WindowScrollerGrid = <D extends any>({ rowSize, colSize, items, chi
|
||||
scrollingDelay: 0
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
virtualizer.measure();
|
||||
}, [rowSize, colSize, virtualizer]);
|
||||
|
||||
return (<div ref={listRef} className={width <= 0 ? `invisible` : ''}>
|
||||
{width > 0 && <div className="w-full relative" style={{
|
||||
height: `${virtualizer.getTotalSize()}px`
|
||||
|
@ -6,6 +6,13 @@ export const CHUNI_MUSIC_PROPERTIES = ['music.songId',
|
||||
'music.worldsEndTag',
|
||||
'music.genre',
|
||||
'music.version',
|
||||
'music.level'
|
||||
'music.level',
|
||||
'musicExt.chartDesigner',
|
||||
'musicExt.tapJudgeCount',
|
||||
'musicExt.holdJudgeCount',
|
||||
'musicExt.slideJudgeCount',
|
||||
'musicExt.airJudgeCount',
|
||||
'musicExt.flickJudgeCount',
|
||||
'musicExt.allJudgeCount'
|
||||
// sql<string>`CAST(music.level AS DECIMAL(3, 1))`.as('level')
|
||||
] as const;
|
||||
|
13
src/types/db.d.ts
vendored
13
src/types/db.d.ts
vendored
@ -21,6 +21,18 @@ export interface ActaeonChuniStaticMapIcon {
|
||||
sortName: string | null;
|
||||
}
|
||||
|
||||
export interface ActaeonChuniStaticMusicExt {
|
||||
airJudgeCount: number;
|
||||
allJudgeCount: number;
|
||||
chartDesigner: string | null;
|
||||
chartId: number;
|
||||
flickJudgeCount: number;
|
||||
holdJudgeCount: number;
|
||||
slideJudgeCount: number;
|
||||
songId: number;
|
||||
tapJudgeCount: number;
|
||||
}
|
||||
|
||||
export interface ActaeonChuniStaticNamePlate {
|
||||
id: number;
|
||||
imagePath: string | null;
|
||||
@ -3343,6 +3355,7 @@ export interface DB {
|
||||
actaeon_arcade_ext: ActaeonArcadeExt;
|
||||
actaeon_arcade_join_keys: ActaeonArcadeJoinKeys;
|
||||
actaeon_chuni_static_map_icon: ActaeonChuniStaticMapIcon;
|
||||
actaeon_chuni_static_music_ext: ActaeonChuniStaticMusicExt;
|
||||
actaeon_chuni_static_name_plate: ActaeonChuniStaticNamePlate;
|
||||
actaeon_chuni_static_system_voice: ActaeonChuniStaticSystemVoice;
|
||||
actaeon_chuni_static_trophies: ActaeonChuniStaticTrophies;
|
||||
|
@ -18,12 +18,6 @@ const config = {
|
||||
'4xl': '1920px',
|
||||
'5xl': '2560px',
|
||||
'6xl': '3440px'
|
||||
},
|
||||
colors: {
|
||||
'chuni-justice-critical': '#dfb920',
|
||||
'chuni-justice': '#db7814',
|
||||
'chuni-attack': '#61a873',
|
||||
'chuni-miss': '#adadad'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user