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 @@ + + + + + +
+
+ Closet++ +
+
+ + 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 @@ + + +
+
+
+ costume +
+
+ + + + +
+
+ +
+ + 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 + }} + > + 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 }) + } +}