feat: 修复小细节

This commit is contained in:
claude-code-best
2026-04-22 17:59:22 +08:00
parent 02783e4f5d
commit 7813904264
2 changed files with 32 additions and 37 deletions

View File

@@ -4,20 +4,14 @@ import type { SpeciesId, SpriteCache } from '../types'
import { getSpeciesData } from '../dex/species' import { getSpeciesData } from '../dex/species'
import { getSpritesDir } from './storage' import { getSpritesDir } from './storage'
const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/HRKings/pokemonsay-newgenerations/master/pokemons' const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/claude-code-best/pokemonsay-newgenerations/master/pokemons'
/** Mapping of speciesId to cow file prefix */ /**
const COW_FILE_MAP: Record<SpeciesId, string> = { * Build cow file name from dex number: NNN.cow
bulbasaur: '001_bulbasaur', */
ivysaur: '002_ivysaur', function cowFileName(speciesId: SpeciesId): string {
venusaur: '003_venusaur', const { dexNumber } = getSpeciesData(speciesId)
charmander: '004_charmander', return `${String(dexNumber).padStart(3, '0')}.cow`
charmeleon: '005_charmeleon',
charizard: '006_charizard',
squirtle: '007_squirtle',
wartortle: '008_wartortle',
blastoise: '009_blastoise',
pikachu: '025_pikachu',
} }
/** /**
@@ -32,8 +26,7 @@ export function loadSprite(speciesId: SpeciesId): SpriteCache | null {
try { try {
const raw = readFileSync(filePath, 'utf-8') const raw = readFileSync(filePath, 'utf-8')
return JSON.parse(raw) as SpriteCache return JSON.parse(raw) as SpriteCache
} catch (e) { } catch {
console.error(`[buddy] Failed to load sprite cache for ${speciesId}:`, e)
return null return null
} }
} }
@@ -47,10 +40,8 @@ export async function fetchAndCacheSprite(speciesId: SpeciesId): Promise<SpriteC
const cached = loadSprite(speciesId) const cached = loadSprite(speciesId)
if (cached) return cached if (cached) return cached
const cowFileName = COW_FILE_MAP[speciesId] const fileName = cowFileName(speciesId)
if (!cowFileName) return null const url = `${GITHUB_RAW_BASE}/${fileName}`
const url = `${GITHUB_RAW_BASE}/${cowFileName}.cow`
try { try {
const response = await fetch(url) const response = await fetch(url)
@@ -63,7 +54,7 @@ export async function fetchAndCacheSprite(speciesId: SpeciesId): Promise<SpriteC
const sprite: SpriteCache = { const sprite: SpriteCache = {
speciesId, speciesId,
lines, lines,
width: Math.max(...lines.map((l) => stripAnsi(l).length)), width: Math.max(...lines.map(l => stripAnsi(l).length)),
height: lines.length, height: lines.length,
fetchedAt: Date.now(), fetchedAt: Date.now(),
} }
@@ -74,8 +65,7 @@ export async function fetchAndCacheSprite(speciesId: SpeciesId): Promise<SpriteC
writeFileSync(filePath, JSON.stringify(sprite, null, 2)) writeFileSync(filePath, JSON.stringify(sprite, null, 2))
return sprite return sprite
} catch (e) { } catch {
console.error(`[buddy] Failed to fetch sprite for ${speciesId}:`, e)
return null return null
} }
} }
@@ -119,7 +109,7 @@ function convertCowToLines(cowContent: string): string[] {
} }
// Trim trailing whitespace on each line (preserve leading for alignment) // Trim trailing whitespace on each line (preserve leading for alignment)
lines = lines.map((line) => line.trimEnd()) lines = lines.map(line => line.trimEnd())
return lines return lines
} }

View File

@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { Box, Text, Pane, Tab, Tabs, useInput, useTabHeaderFocus, type Color } from '@anthropic/ink'; import { Box, Text, Pane, Tab, Tabs, useInput, type Color } from '@anthropic/ink';
import { useSetAppState } from '../../state/AppState.js'; import { useSetAppState } from '../../state/AppState.js';
import { useKeybinding } from '../../keybindings/useKeybinding.js'; import { useKeybinding } from '../../keybindings/useKeybinding.js';
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
@@ -72,6 +72,20 @@ export function BuddyPanel({ buddyData, spriteLines, onClose }: BuddyPanelProps)
isActive: true, isActive: true,
}); });
// Tab / Shift+Tab to switch between tabs
const TAB_ORDER = ['Buddy', 'PC Box', 'Pokédex', 'Egg']
useInput((_input, key) => {
if (key.tab) {
setSelectedTab(prev => {
const idx = TAB_ORDER.indexOf(prev)
if (key.shift) {
return TAB_ORDER[(idx - 1 + TAB_ORDER.length) % TAB_ORDER.length]!
}
return TAB_ORDER[(idx + 1) % TAB_ORDER.length]!
})
}
});
const updateData = (updated: BuddyData) => { const updateData = (updated: BuddyData) => {
setData(updated); setData(updated);
saveBuddyData(updated); saveBuddyData(updated);
@@ -100,7 +114,7 @@ export function BuddyPanel({ buddyData, spriteLines, onClose }: BuddyPanelProps)
return ( return (
<Pane color="permission"> <Pane color="permission">
<Tabs color="permission" selectedTab={selectedTab} onTabChange={setSelectedTab} initialHeaderFocused={true}> <Tabs color="permission" selectedTab={selectedTab} onTabChange={setSelectedTab} disableNavigation={true}>
{tabs} {tabs}
</Tabs> </Tabs>
</Pane> </Pane>
@@ -125,11 +139,11 @@ function PartyView({
useInput((_input, key) => { useInput((_input, key) => {
if (!isActive) return; if (!isActive) return;
if (_input === 'a' || _input === 'A') { if (key.leftArrow) {
setFocusedSlot(prev => (prev > 0 ? prev - 1 : 5)); setFocusedSlot(prev => (prev > 0 ? prev - 1 : 5));
setTick(t => t + 1); setTick(t => t + 1);
setStatusMsg(null); setStatusMsg(null);
} else if (_input === 'd' || _input === 'D') { } else if (key.rightArrow) {
setFocusedSlot(prev => (prev < 5 ? prev + 1 : 0)); setFocusedSlot(prev => (prev < 5 ? prev + 1 : 0));
setTick(t => t + 1); setTick(t => t + 1);
setStatusMsg(null); setStatusMsg(null);
@@ -205,7 +219,7 @@ function PartyView({
{/* Hint */} {/* Hint */}
<Box justifyContent="center"> <Box justifyContent="center">
<Text color={GRAY} dimColor>a/d navigate · Enter swap · X remove</Text> <Text color={GRAY} dimColor>/ navigate · Enter swap · X remove</Text>
</Box> </Box>
{/* Selected creature detail — key forces remount on slot change */} {/* Selected creature detail — key forces remount on slot change */}
@@ -817,7 +831,6 @@ const PARTY_SLOTS = 6
type Panel = 'party' | 'box' type Panel = 'party' | 'box'
function PcBoxTab({ data, onUpdate, isActive }: { data: BuddyData; onUpdate: (d: BuddyData) => void; isActive: boolean }) { function PcBoxTab({ data, onUpdate, isActive }: { data: BuddyData; onUpdate: (d: BuddyData) => void; isActive: boolean }) {
const { headerFocused, blurHeader, focusHeader } = useTabHeaderFocus()
const [boxIdx, setBoxIdx] = useState(0) const [boxIdx, setBoxIdx] = useState(0)
const [panel, setPanel] = useState<Panel>('box') const [panel, setPanel] = useState<Panel>('box')
const [partyCursor, setPartyCursor] = useState(0) // 0-5 const [partyCursor, setPartyCursor] = useState(0) // 0-5
@@ -825,9 +838,6 @@ function PcBoxTab({ data, onUpdate, isActive }: { data: BuddyData; onUpdate: (d:
const [held, setHeld] = useState<{ id: string; from: Panel; partySlot?: number; boxSlot?: number } | null>(null) const [held, setHeld] = useState<{ id: string; from: Panel; partySlot?: number; boxSlot?: number } | null>(null)
const [statusMsg, setStatusMsg] = useState<string | null>(null) const [statusMsg, setStatusMsg] = useState<string | null>(null)
// Blur header on mount so left/right goes to box grid, not tab switching
React.useEffect(() => { blurHeader() }, [blurHeader])
const partySet = new Set(data.party.filter((id): id is string => id !== null)) const partySet = new Set(data.party.filter((id): id is string => id !== null))
const box = data.boxes[boxIdx]! const box = data.boxes[boxIdx]!
const boxRows = Math.ceil(BOX_SIZE / BOX_COLS) const boxRows = Math.ceil(BOX_SIZE / BOX_COLS)
@@ -839,11 +849,6 @@ function PcBoxTab({ data, onUpdate, isActive }: { data: BuddyData; onUpdate: (d:
useInput((_input, key) => { useInput((_input, key) => {
if (!isActive) return if (!isActive) return
// When header is focused, only handle Tab to give focus back to content
if (headerFocused) {
if (key.tab) { blurHeader() }
return // let Tabs handle left/right
}
// Switch box with ,/. regardless of panel // Switch box with ,/. regardless of panel
if (_input === ',' || _input === '<') { if (_input === ',' || _input === '<') {