diff --git a/bun.lock b/bun.lock
index bd2325c..3d289ab 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,7 +6,7 @@
"dependencies": {
"cheerio": "^1.0.0",
"hangul-js": "^0.2.6",
- "hiroba-js": "^2.3.2",
+ "hiroba-js": "2.4.0",
"lzutf8": "^0.6.3",
"node-hiroba": "^1.7.7",
"svelte-lazy": "^1.2.7",
@@ -1305,7 +1305,7 @@
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
- "hiroba-js": ["hiroba-js@2.3.2", "", { "dependencies": { "cheerio": "^1.0.0-rc.12", "fs": "^0.0.1-security", "jimp": "^1.6.0", "node-html-parser": "^7.0.1", "npm": "^11.4.2", "set-cookie-parser": "^2.7.1" } }, "sha512-puzK9UGlGNr23I4E+CpKssmc4jYrLwZSY5vs6LUc3bDpV6k2/+lS/5tCsH0lSPCmZFBlAuzFHHWVaWeNuOYtOg=="],
+ "hiroba-js": ["hiroba-js@2.4.0", "", { "dependencies": { "cheerio": "^1.0.0-rc.12", "fs": "^0.0.1-security", "jimp": "^1.6.0", "node-html-parser": "^7.0.1", "npm": "^11.4.2", "set-cookie-parser": "^2.7.1" } }, "sha512-b7IT56VAEetajWDfa7+GkCJpWXBr4k0eDqmwM3DtG1GlaaRqShKmBItRiVnkA071cJBhmItZYCv3rbGA+9dzlw=="],
"hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="],
diff --git a/package.json b/package.json
index a35465f..7d92118 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"dependencies": {
"cheerio": "^1.0.0",
"hangul-js": "^0.2.6",
- "hiroba-js": "^2.3.2",
+ "hiroba-js": "2.4.0",
"lzutf8": "^0.6.3",
"node-hiroba": "^1.7.7",
"svelte-lazy": "^1.2.7",
diff --git a/src/components/Closet/Closet-btn.svelte b/src/components/Closet/Closet-btn.svelte
new file mode 100644
index 0000000..2724107
--- /dev/null
+++ b/src/components/Closet/Closet-btn.svelte
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/Closet/Closet.svelte b/src/components/Closet/Closet.svelte
new file mode 100644
index 0000000..edf34a4
--- /dev/null
+++ b/src/components/Closet/Closet.svelte
@@ -0,0 +1,102 @@
+
+
+
+ {#if closet}
+ {#each presets as preset, index}
+ {
+ presets = presets.filter((_, i) => i !== index)
+ }}
+ />
+ {/each}
+
+ {:else}
+ Loading...
+ {/if}
+
+
+
diff --git a/src/components/Closet/CostumePreset.svelte b/src/components/Closet/CostumePreset.svelte
new file mode 100644
index 0000000..5804287
--- /dev/null
+++ b/src/components/Closet/CostumePreset.svelte
@@ -0,0 +1,103 @@
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Closet/PresetEditor.svelte b/src/components/Closet/PresetEditor.svelte
new file mode 100644
index 0000000..fd67cd6
--- /dev/null
+++ b/src/components/Closet/PresetEditor.svelte
@@ -0,0 +1,269 @@
+
+
+
+
+
+
+
+
+
+
+ {#if section === 'color'}
+
+
+ {#each colors as row}
+
+ {#each row as color}
+
{
+ preset.color[colorSection] = color.title
+ }}
+ >
+ {/each}
+
+ {/each}
+
+ {:else}
+
+
+ {#each groupedParts as row}
+
+ {#each row as part ([costumeSection, part])}
+
{
+ preset.costume[costumeSection] = part
+ }}
+ >
+

+
+ {/each}
+
+ {/each}
+
+
+ {/if}
+
+
+
diff --git a/src/components/Hash/HashRouter.svelte b/src/components/Hash/HashRouter.svelte
index f93fe17..a87ce2a 100644
--- a/src/components/Hash/HashRouter.svelte
+++ b/src/components/Hash/HashRouter.svelte
@@ -5,11 +5,13 @@
import { setContext } from 'svelte'
import TrainingAdd from '../Training/pages/add/TrainingAdd.svelte'
import TrainingEdit from '../Training/pages/edit/TrainingEdit.svelte'
+ import Closet from '../Closet/Closet.svelte'
const routes = {
'/training': Training,
'/training/add': TrainingAdd,
- '/training/edit/:hash': TrainingEdit
+ '/training/edit/:hash': TrainingEdit,
+ '/closet': Closet
}
const title = writable('')
@@ -35,5 +37,7 @@
display:flex;
flex-direction: column;
align-items: center;
+ width: 100%;
+ max-width: 300px;
}
diff --git a/src/components/Rating/services/UploadService.ts b/src/components/Rating/services/UploadService.ts
index 77433a3..c87b27d 100644
--- a/src/components/Rating/services/UploadService.ts
+++ b/src/components/Rating/services/UploadService.ts
@@ -7,28 +7,28 @@ import type RecentScoreStorage from '../recentScoreStorage'
import { clearTop50RatingCache } from '../../../lib/rating'
import { DonderHiroba, type DaniNo, type CardData, type ScoreData, type Summary, type Difficulty } from 'hiroba-js'
-type TaikoProfile = {
- taikoNo: string;
- nickname: string;
+interface TaikoProfile {
+ taikoNo: string
+ nickname: string
crown: {
- donderfull: number;
- gold: number;
- silver: number;
- };
+ donderfull: number
+ gold: number
+ silver: number
+ }
badge: {
- rainbow: number;
- purple: number;
- pink: number;
- gold: number;
- silver: number;
- bronze: number;
- white: number;
- };
+ rainbow: number
+ purple: number
+ pink: number
+ gold: number
+ silver: number
+ bronze: number
+ white: number
+ }
dani: {
- dan: "5kyu" | "4kyu" | "3kyu" | "2kyu" | "1kyu" | "1dan" | "2dan" | "3dan" | "4dan" | "5dan" | "6dan" | "7dan" | "8dan" | "9dan" | "10dan" | "kuroto" | "meijin" | "chojin" | "tatsujin";
- type: "gold" | "red";
- frame: "gold" | "silver" | "rainbow";
- } | null;
+ dan: '5kyu' | '4kyu' | '3kyu' | '2kyu' | '1kyu' | '1dan' | '2dan' | '3dan' | '4dan' | '5dan' | '6dan' | '7dan' | '8dan' | '9dan' | '10dan' | 'kuroto' | 'meijin' | 'chojin' | 'tatsujin'
+ type: 'gold' | 'red'
+ frame: 'gold' | 'silver' | 'rainbow'
+ } | null
}
export class UploadService {
@@ -40,7 +40,6 @@ export class UploadService {
return wikiUser.logined === true
}
-
async fetchScoreForSong (songNo: string, clearData: ClearData): Promise {
const response: { songNo: string, body: { oni?: string, ura?: string } } =
{ songNo, body: {} }
@@ -86,29 +85,35 @@ export class UploadService {
): Promise {
let data
- const fetchDani = async (cardData: CardData & { summary?: Summary }) => {
- const daniPass = await DonderHiroba.func.getDaniPass({ taikoNo: cardData.taikoNumber });
- const REGULAR_DAN = ["senpo", "jiho", "chiuken", "fukusho", "taisho", "beginner", "10kyu", "9kyu", "8kyu", "7kyu", "6kyu", "5kyu", "4kyu", "3kyu", "2kyu", "1kyu", "1dan", "2dan", "3dan", "4dan", "5dan", "6dan", "7dan", "8dan", "9dan", "10dan", "kuroto", "meijin", "chojin", "tatsujin"] as const;
+ type ReturnType = Promise<{
+ dan: '5kyu' | '4kyu' | '3kyu' | '2kyu' | '1kyu' | '1dan' | '2dan' | '3dan' | '4dan' | '5dan' | '6dan' | '7dan' | '8dan' | '9dan' | '10dan' | 'kuroto' | 'meijin' | 'chojin' | 'tatsujin'
+ type: 'gold' | 'red'
+ frame: 'gold' | 'silver' | 'rainbow'
+ } | null>
+
+ const fetchDani = async (cardData: CardData & { summary?: Summary }): ReturnType => {
+ const daniPass = await DonderHiroba.func.getDaniPass({ taikoNo: cardData.taikoNumber })
+ const REGULAR_DAN = ['senpo', 'jiho', 'chiuken', 'fukusho', 'taisho', 'beginner', '10kyu', '9kyu', '8kyu', '7kyu', '6kyu', '5kyu', '4kyu', '3kyu', '2kyu', '1kyu', '1dan', '2dan', '3dan', '4dan', '5dan', '6dan', '7dan', '8dan', '9dan', '10dan', 'kuroto', 'meijin', 'chojin', 'tatsujin'] as const
for (let i = 19; i >= 1; i--) {
- const danino = i as DaniNo;
- const pass = daniPass[danino];
- if (!pass) continue;
+ const danino = i as DaniNo
+ const pass = daniPass[danino]
+ if (pass === undefined || pass === null) continue
const dani = {
dan: REGULAR_DAN[i + 10],
type: pass.pass,
- frame: pass.frame === "donderfull" ? 'rainbow' : pass.frame
- };
- return dani as TaikoProfile['dani'];
+ frame: pass.frame === 'donderfull' ? 'rainbow' : pass.frame
+ }
+ return dani as TaikoProfile['dani']
}
- return null;
+ return null
}
if (cardData === null) {
alert('Donder Hiroba data is required to upload to Taiko.wiki rating.')
- return;
+ return
}
- const dani = await fetchDani(cardData);
+ const dani = await fetchDani(cardData)
const taikoProfile: TaikoProfile = {
nickname: cardData.nickname,
@@ -146,7 +151,7 @@ export class UploadService {
if (scoreDataMap !== undefined) {
for (const song of clearData) {
- let scoreData = scoreDataMap[song.songNo]
+ const scoreData = scoreDataMap[song.songNo]
const difficulties: Difficulty[] = ['easy', 'normal', 'hard', 'oni', 'ura']
for (const diff of difficulties) {
if (scoreData?.difficulty[diff] !== undefined) {
@@ -160,12 +165,12 @@ export class UploadService {
}
await fetch('https://rating.taiko.wiki/api/v1/rating/upload', {
- method: 'post',
- body: lzutf8.compress(data, { outputEncoding: 'Base64' }),
- credentials: 'include',
- headers: {
- 'content-type': 'application/json'
- }
+ method: 'post',
+ body: lzutf8.compress(data, { outputEncoding: 'Base64' }),
+ credentials: 'include',
+ headers: {
+ 'content-type': 'application/json'
+ }
})
}
@@ -240,7 +245,7 @@ export class UploadService {
const scoreData = prevScoreDataMap[songNos[0]]
if (item.scoreData.count.play ===
- scoreData?.difficulty?.[item.difficulty]?.count.play) {
+ scoreData?.difficulty?.[item.difficulty]?.count.play) {
shouldStop = true
}
}
diff --git a/src/injection.ts b/src/injection.ts
index 3d73fa4..723c5dc 100644
--- a/src/injection.ts
+++ b/src/injection.ts
@@ -10,6 +10,7 @@ import dani from './injections/dani'
import hash, { hashChangeCallback } from './injections/hash'
import { isIOS } from '../src/lib/utils'
import { SettingsStorage } from './lib/settings'
+import mypage_kisekae_favorite_save from './injections/mypage_kisekae_favorite_save'
const runHiroba = async (): Promise => {
window.addEventListener('hashchange', hashChangeCallback)
@@ -28,7 +29,8 @@ const runHiroba = async (): Promise => {
'favorite_song_select.php': favorite_song_select,
'score_detail.php': score_detail,
'select_song.php': select_song,
- 'dan_detail.php': dani
+ 'dan_detail.php': dani,
+ 'mypage_kisekae_favorite_save.php': mypage_kisekae_favorite_save
}
if (page.startsWith('#')) {
diff --git a/src/injections/mypage_kisekae_favorite_save.ts b/src/injections/mypage_kisekae_favorite_save.ts
new file mode 100644
index 0000000..4ea1a7a
--- /dev/null
+++ b/src/injections/mypage_kisekae_favorite_save.ts
@@ -0,0 +1,10 @@
+import ClosetBtn from '../components/Closet/Closet-btn.svelte'
+
+export default async function (): Promise {
+ const target = document.querySelector('.favoriteArea.tmp')
+ if (target === null) return
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const _ = new ClosetBtn({
+ target
+ })
+}
diff --git a/src/injections/taikowiki.ts b/src/injections/taikowiki.ts
index 984e430..d6c88a0 100644
--- a/src/injections/taikowiki.ts
+++ b/src/injections/taikowiki.ts
@@ -17,7 +17,7 @@ const insertContextMenu = (playlistsStore: PlaylistsStore, recentScoreStorage: R
const linkElem = target.closest('a')
if (linkElem === null) return
- const title = linkElem.querySelector('.title')?.textContent?.trim() || ''
+ const title = linkElem?.querySelector('.title')?.textContent?.trim?.() ?? ''
try {
const url = new URL(linkElem.href)
@@ -29,7 +29,7 @@ const insertContextMenu = (playlistsStore: PlaylistsStore, recentScoreStorage: R
if (songNo === undefined) return
const diffFromParam = url.searchParams.get('diff')
- let diff: Difficulty = 'oni'; // or another appropriate default value
+ let diff: Difficulty = 'oni' // or another appropriate default value
if (diffFromParam === 'ura' || diffFromParam === 'oni_ura') diff = 'ura'
else if (diffFromParam === 'oni') diff = 'oni'
@@ -43,7 +43,7 @@ const insertContextMenu = (playlistsStore: PlaylistsStore, recentScoreStorage: R
target: document.body,
props: {
playlists: playlistsStore,
- title: title,
+ title,
recentScores: recentScoreStorage,
difficulty: diff,
songNo,
@@ -76,7 +76,7 @@ export default async (): Promise => {
const recentScoreStore = new RecentScoreStorage()
await recentScoreStore.loadFromChromeStorage()
- const runInterval = () => {
+ const runInterval = (): void => {
let executionCount = 0
const intervalId = setInterval(() => {
insertContextMenu(playlistsStore, recentScoreStore)
diff --git a/src/lib/closet.ts b/src/lib/closet.ts
new file mode 100644
index 0000000..3046e8f
--- /dev/null
+++ b/src/lib/closet.ts
@@ -0,0 +1,40 @@
+import type { KisekaeReqData } from 'hiroba-js'
+
+const STORAGE_KEY = 'closet'
+
+export class Closet {
+ private static instance: Closet | null = null
+ static async getInstance (): Promise {
+ if (this.instance !== null) {
+ return this.instance
+ }
+
+ this.instance = new Closet()
+ await this.instance.load()
+ return this.instance
+ }
+
+ presets: KisekaeReqData[] = []
+
+ private async load (): Promise {
+ const storage = chrome?.storage?.local
+ if (storage === undefined) {
+ return
+ }
+
+ const data = (await storage.get(STORAGE_KEY))[STORAGE_KEY] as KisekaeReqData[]
+ if (Array.isArray(data)) {
+ this.presets = [...data]
+ }
+ }
+
+ async save (): Promise {
+ const storage = chrome?.storage?.local
+ if (storage === undefined) {
+ console.warn('storage is not available')
+ return
+ }
+
+ await storage.set({ [STORAGE_KEY]: this.presets })
+ }
+}