Function bodies 255 total
ViewLoader function · typescript · L26-L32 (7 LOC)src/App.tsx
function ViewLoader() {
return (
<div className="h-full flex items-center justify-center">
<div className="text-[--color-text-muted] text-sm">Loading...</div>
</div>
);
}formatMinutesToTime function · typescript · L50-L61 (12 LOC)src/components/Comparison/ComparisonCharts.tsx
function formatMinutesToTime(minutes: number): string {
const totalSeconds = Math.round(Math.abs(minutes) * 60);
const hrs = Math.floor(totalSeconds / 3600);
const mins = Math.floor((totalSeconds % 3600) / 60);
const secs = totalSeconds % 60;
const sign = minutes < 0 ? '-' : '';
if (hrs > 0) {
return `${sign}${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
return `${sign}${mins}:${secs.toString().padStart(2, '0')}`;
}formatDeltaMs function · typescript · L63-L70 (8 LOC)src/components/Comparison/ComparisonCharts.tsx
function formatDeltaMs(ms: number): string {
const sign = ms >= 0 ? '+' : '-';
const absMs = Math.abs(ms);
const totalSeconds = Math.floor(absMs / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${sign}${minutes}:${seconds.toString().padStart(2, '0')}`;
}formatTime function · typescript · L668-L678 (11 LOC)src/components/Comparison/ComparisonView.tsx
function formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}formatDelta function · typescript · L680-L688 (9 LOC)src/components/Comparison/ComparisonView.tsx
function formatDelta(ms: number): string {
const sign = ms >= 0 ? '+' : '-';
const absMs = Math.abs(ms);
const totalSeconds = Math.floor(absMs / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${sign}${minutes}:${seconds.toString().padStart(2, '0')}`;
}AddMemberForm function · typescript · L10-L61 (52 LOC)src/components/Group/AddMemberForm.tsx
export function AddMemberForm({ memberCount }: AddMemberFormProps) {
const [accountName, setAccountName] = useState('');
const [error, setError] = useState<string | null>(null);
const [isAdding, setIsAdding] = useState(false);
const { addMember } = useGroupStore();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const trimmed = accountName.trim();
if (!trimmed) return;
if (memberCount >= 5) {
setError('Maximum of 5 group members allowed');
return;
}
setIsAdding(true);
setError(null);
try {
await addMember({ accountName: trimmed });
setAccountName('');
} catch (err) {
setError(String(err));
} finally {
setIsAdding(false);
}
};
return (
<form onSubmit={handleSubmit} className="flex gap-2 items-end">
<div className="flex-1">
<input
type="text"
value={accountName}
onChange={(e) => setAccountName(e.target.value)}
GroupMemberCard function · typescript · L13-L154 (142 LOC)src/components/Group/GroupMemberCard.tsx
export function GroupMemberCard({ member }: GroupMemberCardProps) {
const { updateMember, removeMember, setMemberActive } = useGroupStore();
const detectionStatus = useGroupStore((s) => s.detectionStatus[member.id]);
const [isEditing, setIsEditing] = useState(false);
const [editCharName, setEditCharName] = useState(member.characterName || '');
const [isResolving, setIsResolving] = useState(false);
const [resolvedChars, setResolvedChars] = useState<PoeCharacter[] | null>(null);
const handleResolve = async () => {
setIsResolving(true);
try {
const characters = await invoke<PoeCharacter[]>('resolve_group_member_characters', {
accountName: member.accountName,
});
setResolvedChars(characters);
} catch (err) {
console.error('Failed to resolve characters:', err);
} finally {
setIsResolving(false);
}
};
const handleSelectCharacter = async (charName: string) => {
await updateMember(member.id, charName, member.dispProvenance: Repobility (https://repobility.com) — every score reproducible from /scan/
formatTime function · typescript · L9-L18 (10 LOC)src/components/Group/GroupRunHistory.tsx
function formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
return `${minutes}:${String(seconds).padStart(2, '0')}`;
}GroupView function · typescript · L9-L62 (54 LOC)src/components/Group/GroupView.tsx
export function GroupView() {
const { members, loadMembers } = useGroupStore();
useEffect(() => {
loadMembers();
}, [loadMembers]);
return (
<div className="h-full flex flex-col p-6">
<div className="mb-4">
<h1 className="text-2xl font-bold text-[--color-text] flex items-center gap-2">
<Users className="w-6 h-6" />
Group Mode
<HelpTip>
Track up to 5 party members during group speedruns. Each member's progress is tracked independently. Enable Group Mode in Settings first, then add members here.
</HelpTip>
</h1>
<p className="text-[--color-text-muted] mt-1">
Manage party members and view group run history
</p>
</div>
<div className="flex-1 overflow-auto space-y-6">
{/* Group Members Section */}
<section>
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-[--color-text]">
formatTime function · typescript · L259-L269 (11 LOC)src/components/History/AnalyticsTab.tsx
function formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}HistoryView function · typescript · L14-L109 (96 LOC)src/components/History/HistoryView.tsx
export function HistoryView() {
const [activeTab, setActiveTab] = useState<TabType>('runs');
const [showAddReferenceModal, setShowAddReferenceModal] = useState(false);
const { filters, setFilters, clearFilters, loadFilteredRuns, loadRunStats, loadSplitStats } =
useRunStore();
// Load data when filters change
useEffect(() => {
loadFilteredRuns();
loadRunStats();
loadSplitStats();
}, [filters, loadFilteredRuns, loadRunStats, loadSplitStats]);
const handleFiltersChange = (newFilters: Partial<RunFilters>) => {
setFilters(newFilters);
};
const handleClearFilters = () => {
clearFilters();
};
return (
<div className="h-full flex flex-col p-6">
<div className="mb-4">
<h1 className="text-2xl font-bold text-[--color-text] flex items-center gap-2">
Run History
<HelpTip>
Browse all completed and abandoned runs with detailed split times, statistics, and trends over time. Click a run to view its splitformatTime function · typescript · L283-L293 (11 LOC)src/components/History/RunsTab.tsx
function formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}formatPbTime function · typescript · L21-L31 (11 LOC)src/components/Overlay/OverlayBreakpoints.tsx
function formatPbTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}formatDelta function · typescript · L33-L45 (13 LOC)src/components/Overlay/OverlayBreakpoints.tsx
function formatDelta(ms: number): string {
const absMs = Math.abs(ms);
const totalSeconds = Math.floor(absMs / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
// Positive timeUntilPb means ahead (still have time), negative means behind
const sign = ms >= 0 ? '-' : '+';
if (minutes > 0) {
return `${sign}${minutes}:${seconds.toString().padStart(2, '0')}`;
}
return `${sign}0:${seconds.toString().padStart(2, '0')}`;
}OverlayBreakpoints function · typescript · L47-L118 (72 LOC)src/components/Overlay/OverlayBreakpoints.tsx
export function OverlayBreakpoints({ breakpoints, maxCount = 3, fontSize = 'medium', startTime, elapsedMs, isRunning }: OverlayBreakpointsProps) {
const visibleBreakpoints = breakpoints.slice(0, maxCount);
const itemClass = fontSize === 'small' ? 'text-[10px]' : fontSize === 'large' ? 'text-sm' : 'text-xs';
// Tick every second when running so countdowns update live
const [tick, setTick] = useState(0);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
if (isRunning && startTime != null) {
intervalRef.current = setInterval(() => setTick((t) => t + 1), 1000);
}
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [isRunning, startTime]);
// Suppress unused warning - tick drives re-renders
void tick;
const currentElapsed = isRunning && startTime ? Date.now() - startTime : elapsedMs;
return (
<div className="pt-1" style={{ borderTop: '1px solid rgba(58, 58, 62, 0Open data scored by Repobility · https://repobility.com
formatDelta function · typescript · L13-L22 (10 LOC)src/components/Overlay/OverlaySplit.tsx
function formatDelta(ms: number): string {
const absMs = Math.abs(ms);
const totalSeconds = Math.floor(absMs / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const centiseconds = Math.floor((absMs % 1000) / 10);
const sign = ms >= 0 ? '+' : '-';
return `${sign}${minutes}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
}formatSplitTime function · typescript · L24-L34 (11 LOC)src/components/Overlay/OverlaySplit.tsx
function formatSplitTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}formatSegmentTime function · typescript · L36-L45 (10 LOC)src/components/Overlay/OverlaySplit.tsx
function formatSegmentTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
if (minutes > 0) {
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
return `${seconds}s`;
}OverlaySplit function · typescript · L47-L98 (52 LOC)src/components/Overlay/OverlaySplit.tsx
export function OverlaySplit({ name, deltaMs, isBestSegment, splitTimeMs, segmentTimeMs, pbSegmentTimeMs, goldSegmentTimeMs, fontSize = 'medium', scale = 'medium' }: OverlaySplitProps) {
let deltaColor = '#9a8e82'; // neutral
if (isBestSegment) {
deltaColor = '#fbbf24'; // gold
} else if (deltaMs !== null) {
deltaColor = deltaMs < 0 ? '#22c55e' : '#d4a574'; // ahead / behind (amber)
}
const sizeClass = fontSize === 'small' ? 'text-xs' : fontSize === 'large' ? 'text-base' : 'text-sm';
const detailSizeClass = fontSize === 'small' ? 'text-[9px]' : fontSize === 'large' ? 'text-sm' : 'text-xs';
const ptClass = scale === 'small' ? 'pt-1' : 'pt-2';
// Build segment comparison text
const hasComparison = segmentTimeMs !== undefined && (pbSegmentTimeMs || goldSegmentTimeMs);
return (
<div className={`${ptClass}`} style={{ borderTop: '1px solid rgba(58, 58, 62, 0.5)' }}>
<div className={`flex items-center justify-between ${sizeClass}`}>
<span classformatTime function · typescript · L10-L21 (12 LOC)src/components/Overlay/OverlayTimer.tsx
function formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const centiseconds = Math.floor((ms % 1000) / 10);
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
}
return `${minutes}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
}OverlayTimer function · typescript · L23-L59 (37 LOC)src/components/Overlay/OverlayTimer.tsx
export function OverlayTimer({ startTime, elapsedMs, isRunning, fontSize = 'medium' }: OverlayTimerProps) {
const [displayMs, setDisplayMs] = useState(elapsedMs);
const animationRef = useRef<number | null>(null);
// Run our own animation loop to compute elapsed time locally
useEffect(() => {
if (isRunning && startTime) {
const updateTimer = () => {
setDisplayMs(Date.now() - startTime);
animationRef.current = requestAnimationFrame(updateTimer);
};
animationRef.current = requestAnimationFrame(updateTimer);
} else {
// When not running, use the last known elapsedMs from the main window
setDisplayMs(elapsedMs);
}
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [isRunning, startTime, elapsedMs]);
const timerSizeClass = fontSize === 'small' ? 'text-xl' : fontSize === 'large' ? 'text-4xl' : 'text-3xl';
return (
<div className="text-center">OverlayZone function · typescript · L9-L25 (17 LOC)src/components/Overlay/OverlayZone.tsx
export function OverlayZone({ zoneName, fontSize = 'medium', isAhead, hotkeyHint, showHotkeyHint }: OverlayZoneProps) {
const sizeClass = fontSize === 'small' ? 'text-xs' : fontSize === 'large' ? 'text-base' : 'text-sm';
// Green when ahead of PB, amber when behind or no data; muted when empty
const color = !zoneName ? '#4a4440' : isAhead === undefined ? '#9a8e82' : isAhead ? '#22c55e' : '#d4a574';
const placeholder = showHotkeyHint
? `${hotkeyHint || 'Ctrl+Space'} to start`
: 'Waiting for zone...';
return (
<div className="text-center">
<div className={`${sizeClass} truncate`} style={{ color }} title={zoneName || undefined}>
{zoneName || placeholder}
</div>
</div>
);
}PracticeHistory function · typescript · L5-L128 (124 LOC)src/components/Practice/PracticeHistory.tsx
export function PracticeHistory() {
const { attempts, bestTimeMs, clearAttempts, mode, selectedZones } = usePracticeStore();
// Calculate stats
const avgTimeMs = attempts.length > 0
? attempts.reduce((sum, a) => sum + a.timeMs, 0) / attempts.length
: null;
const totalDeaths = attempts.reduce((sum, a) => sum + a.deathCount, 0);
return (
<div className="card-inset rounded-lg h-full flex flex-col">
<div className="p-4 section-header rounded-t-lg flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-[--color-text]">Attempts</h2>
<p className="text-xs text-[--color-text-muted] mt-1">
{attempts.length} attempt{attempts.length !== 1 ? 's' : ''}
</p>
</div>
{attempts.length > 0 && (
<button
onClick={clearAttempts}
className="p-1.5 rounded-md text-[--color-text-muted] hover:text-red-400 transition-colors"
title="Clear all attAll rows scored by the Repobility analyzer (https://repobility.com)
formatTime function · typescript · L130-L141 (12 LOC)src/components/Practice/PracticeHistory.tsx
function formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const centiseconds = Math.floor((ms % 1000) / 10);
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
}
return `${minutes}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
}PracticeTimer function · typescript · L7-L173 (167 LOC)src/components/Practice/PracticeTimer.tsx
export function PracticeTimer() {
const {
mode, selectedZones, timer, attempts, bestTimeMs,
stopPractice, resetPractice, updateElapsed,
} = usePracticeStore();
const currentZone = useRunStore((s) => s.timer.currentZone);
const animationRef = useRef<number | null>(null);
// Update timer every frame
useEffect(() => {
const tick = () => {
if (timer.isRunning && timer.startTime) {
updateElapsed(Date.now() - timer.startTime);
}
animationRef.current = requestAnimationFrame(tick);
};
if (timer.isRunning) {
animationRef.current = requestAnimationFrame(tick);
}
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [timer.isRunning, timer.startTime, updateElapsed]);
const handleBack = () => {
stopPractice();
resetPractice();
};
return (
<div className="flex flex-col gap-4 h-full">
{/* Header with back button */}
<div classNaPracticeInfo function · typescript · L175-L210 (36 LOC)src/components/Practice/PracticeTimer.tsx
function PracticeInfo() {
const mode = usePracticeStore((s) => s.mode);
const selectedZones = usePracticeStore((s) => s.selectedZones);
const getExitZone = usePracticeStore((s) => s.getExitZone);
const getRouteExitZone = usePracticeStore((s) => s.getRouteExitZone);
const isSingleZone = mode === 'single_zone';
const exitZone = isSingleZone ? getExitZone() : getRouteExitZone();
const startZone = selectedZones[0];
return (
<div className="card-inset rounded-lg p-3">
<div className="flex items-center gap-2 text-sm">
<span className="text-[--color-text-muted]">
{isSingleZone ? 'Practicing:' : 'Starts on:'}
</span>
<span className="text-[--color-poe-gold] font-medium">{startZone.name}</span>
<span className="text-[--color-text-muted] text-xs">(A{startZone.act})</span>
</div>
{exitZone ? (
<div className="flex items-center gap-2 text-sm mt-1.5">
<span className="text-[--color-text-muted]">ComplformatTime function · typescript · L212-L223 (12 LOC)src/components/Practice/PracticeTimer.tsx
function formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const centiseconds = Math.floor((ms % 1000) / 10);
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
}
return `${minutes}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
}PracticeView function · typescript · L8-L48 (41 LOC)src/components/Practice/PracticeView.tsx
export function PracticeView() {
const { selectedZones, isActive, loadFromStorage } = usePracticeStore();
useEffect(() => {
loadFromStorage();
}, [loadFromStorage]);
return (
<div className="h-full flex flex-col p-6">
<div className="mb-6">
<h1 className="text-2xl font-bold text-[--color-text] flex items-center gap-2" style={{ textShadow: '0 0 30px rgba(175, 96, 37, 0.2)' }}>
Practice Mode
<HelpTip>
Practice zone layouts on a high-level, fast character. Run the same zones repeatedly to learn tileset layouts and optimize pathing. Your times are saved so you can track improvement across many attempts.
</HelpTip>
</h1>
<p className="text-[--color-text-muted] mt-1 text-sm">
Practice individual zones or run through a sequence of zones.
</p>
</div>
<div className="flex-1 flex gap-6 min-h-0">
{/* Left side - Zone selector and controls */}
<div className="flegetSelectableZones function · typescript · L9-L22 (14 LOC)src/components/Practice/ZoneSelector.tsx
function getSelectableZones(): PracticeZone[] {
const zones: PracticeZone[] = [];
for (const bp of defaultBreakpoints) {
// Only include zone-trigger breakpoints (not level milestones or kitava triggers)
if (bp.trigger.type === 'zone' && bp.trigger.zoneName && bp.trigger.act) {
zones.push({
name: bp.name,
zoneName: bp.trigger.zoneName,
act: bp.trigger.act,
});
}
}
return zones;
}ZoneSelector function · typescript · L27-L223 (197 LOC)src/components/Practice/ZoneSelector.tsx
export function ZoneSelector() {
const { mode, setMode, selectedZones, addZone, removeZone, moveZone, clearZones, startPractice } = usePracticeStore();
const [search, setSearch] = useState('');
const [actFilter, setActFilter] = useState<number | null>(null);
const filteredZones = useMemo(() => {
let zones = allZones;
if (actFilter !== null) {
zones = zones.filter(z => z.act === actFilter);
}
if (search) {
const lower = search.toLowerCase();
zones = zones.filter(z => z.name.toLowerCase().includes(lower) || z.zoneName.toLowerCase().includes(lower));
}
return zones;
}, [search, actFilter]);
const selectedZoneNames = new Set(selectedZones.map(z => `${z.zoneName}-${z.act}`));
return (
<div className="flex flex-col gap-4 h-full min-h-0">
{/* Mode toggle */}
<div className="flex gap-2">
<button
onClick={() => setMode('single_zone')}
className={`flex-1 flex items-center justify-center gap-2 pgetStepSummary function · typescript · L12-L30 (19 LOC)src/components/Settings/BreakpointWizard.tsx
function getStepSummary(step: number, config: WizardConfig): string {
switch (step) {
case 1:
return config.endAct === 0 ? 'Dev Test' : `Act ${config.endAct} ${config.runType === 'any_percent' ? 'Any%' : '100%'}`;
case 2:
return ({
every_zone: 'Every Zone',
key_zones: 'Key Zones',
bosses_only: 'Bosses Only',
acts_only: 'Acts Only',
} as Record<string, string>)[config.verbosity] ?? config.verbosity;
case 3: {
const freq = config.snapshotFrequency || 'acts_only';
return freq === 'bosses_only' ? 'Bosses + Acts' : 'Acts Only';
}
default:
return '';
}
}Same scanner, your repo: https://repobility.com — Repobility
BreakpointWizard function · typescript · L32-L181 (150 LOC)src/components/Settings/BreakpointWizard.tsx
export function BreakpointWizard() {
const { wizardConfig, setWizardConfig } = useSettingsStore();
// Local wizard state (committed on Apply)
// Migrate older configs that may lack snapshotFrequency
const [config, setConfig] = useState<WizardConfig>(() => {
const saved = wizardConfig ?? DEFAULT_WIZARD_CONFIG;
if (!saved.snapshotFrequency) {
return { ...saved, snapshotFrequency: 'acts_only' as const };
}
return saved;
});
const [step, setStep] = useState<Step>(1);
// Preview: generate breakpoints from current local config
const preview = useMemo(() => generateBreakpoints(config), [config]);
const enabledCount = useMemo(() => preview.filter(bp => bp.isEnabled).length, [preview]);
const snapshotCount = useMemo(() => preview.filter(bp => bp.isEnabled && bp.captureSnapshot).length, [preview]);
const grouped = useMemo(() => groupByAct(preview), [preview]);
const handleApply = () => {
setWizardConfig(config);
try {
localStorage.seRouteCustomizations function · typescript · L353-L498 (146 LOC)src/components/Settings/BreakpointWizard.tsx
export function RouteCustomizations() {
const { wizardConfig, setWizardConfig } = useSettingsStore();
if (!wizardConfig) {
return (
<p className="text-sm text-[--color-text-muted] p-4">
Configure the wizard above first to enable route customizations.
</p>
);
}
const setRoute = <K extends keyof WizardConfig['routes']>(key: K, value: WizardConfig['routes'][K]) => {
const updated = { ...wizardConfig, routes: { ...wizardConfig.routes, [key]: value } };
setWizardConfig(updated);
try {
localStorage.setItem('poe-watcher-wizard-config', JSON.stringify(updated));
} catch (e) {
console.error('Failed to save wizard config:', e);
}
};
const showAct6Plus = wizardConfig.endAct === 10;
return (
<div className="space-y-3">
{/* Act 1 */}
<RouteSection title="Act 1">
<RadioOption
label="Standard"
desc="Skip or delay Dweller of the Deep"
selected={wizardConfig.routes.act1 ==RouteSection function · typescript · L502-L521 (20 LOC)src/components/Settings/BreakpointWizard.tsx
function RouteSection({ title, children }: { title: string; children: React.ReactNode }) {
const [open, setOpen] = useState(true);
return (
<div className="border border-[--color-border] rounded-lg overflow-hidden">
<button
onClick={() => setOpen(!open)}
className="w-full flex items-center justify-between p-2.5 text-sm font-medium text-[--color-text] hover:bg-[--color-surface-elevated]/50 active:scale-[0.99] transition-all"
>
<span>{title}</span>
<svg
className={`w-4 h-4 text-[--color-text-muted] transition-transform ${open ? 'rotate-180' : ''}`}
fill="none" stroke="currentColor" viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{open && <div className="px-3 pb-3 space-y-1.5">{children}</div>}
</div>
);
}mapKeyToTauri function · typescript · L11-L42 (32 LOC)src/components/Settings/HotkeyInput.tsx
function mapKeyToTauri(key: string): string | null {
// Ignore standalone modifier keys
if (['Control', 'Shift', 'Alt', 'Meta'].includes(key)) return null;
const keyMap: Record<string, string> = {
' ': 'Space',
'ArrowUp': 'Up',
'ArrowDown': 'Down',
'ArrowLeft': 'Left',
'ArrowRight': 'Right',
'Enter': 'Enter',
'Escape': 'Escape',
'Tab': 'Tab',
'Backspace': 'Backspace',
'Delete': 'Delete',
'Home': 'Home',
'End': 'End',
'PageUp': 'PageUp',
'PageDown': 'PageDown',
'Insert': 'Insert',
};
if (keyMap[key]) return keyMap[key];
// Function keys
if (/^F\d{1,2}$/.test(key)) return key;
// Single character keys - uppercase
if (key.length === 1) return key.toUpperCase();
return key;
}HotkeyInput function · typescript · L44-L115 (72 LOC)src/components/Settings/HotkeyInput.tsx
export function HotkeyInput({ value, onChange, error }: HotkeyInputProps) {
const [capturing, setCapturing] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
const handleKeyDown = useCallback((e: KeyboardEvent) => {
e.preventDefault();
e.stopPropagation();
// Escape cancels capture
if (e.key === 'Escape') {
setCapturing(false);
return;
}
const tauriKey = mapKeyToTauri(e.key);
if (!tauriKey) return; // Ignore lone modifier presses
// Require at least one modifier
if (!e.ctrlKey && !e.shiftKey && !e.altKey) return;
// Build shortcut string in Tauri format
const parts: string[] = [];
if (e.ctrlKey) parts.push('Ctrl');
if (e.shiftKey) parts.push('Shift');
if (e.altKey) parts.push('Alt');
parts.push(tauriKey);
const shortcut = parts.join('+');
onChange(shortcut);
setCapturing(false);
}, [onChange]);
useEffect(() => {
if (capturing) {
// Suspend OS-level global sgetTypeIcon function · typescript · L35-L47 (13 LOC)src/components/Settings/SettingsView.tsx
function getTypeIcon(type: BreakpointType | string): ReactNode {
const cls = 'w-4 h-4';
const sw = 1.75;
switch (type) {
case 'zone': return <MapPin className={cls} strokeWidth={sw} />;
case 'level': return <ArrowUp className={cls} strokeWidth={sw} />;
case 'boss': return <Skull className={cls} strokeWidth={sw} />;
case 'act': return <Landmark className={cls} strokeWidth={sw} />;
case 'lab': return <Trophy className={cls} strokeWidth={sw} />;
case 'custom': return <Star className={cls} strokeWidth={sw} />;
default: return <span>•</span>;
}
}GroupModeSection function · typescript · L431-L454 (24 LOC)src/components/Settings/SettingsView.tsx
function GroupModeSection() {
const groupModeEnabled = useSettingsStore((s) => s.groupModeEnabled);
const setGroupModeEnabled = useSettingsStore((s) => s.setGroupModeEnabled);
return (
<section>
<h2 className="text-lg font-semibold text-[--color-text] mb-4 flex items-center gap-2 flex-wrap">
Group Mode
<HelpTip>
Group Mode tracks up to 5 party members during group speedruns. Each member's progress is tracked independently. Enable this before starting a group run, then configure members in the Group tab.
</HelpTip>
</h2>
<div className="card-inset rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="text-[--color-text]">Enable Group Mode</div>
<div className="text-xs text-[--color-text-muted]">Track up to 5 party members during group speedruns. Configure members in the Group tab.</div>
</div>
<Toggle checked={groupModeEnabled}GeneralTab function · typescript · L481-L668 (188 LOC)src/components/Settings/SettingsView.tsx
function GeneralTab({
poeLogPath,
accountName,
testCharacterName,
checkUpdates,
setLogPath,
setAccountName,
setTestCharacterName,
setCheckUpdates,
handleBrowseLogPath,
handleDetectLogPath,
handleSaveSettings,
saveStatus,
checking,
available,
version,
updateError,
checkForUpdate,
downloadAndInstall,
downloading,
progress,
}: GeneralTabProps) {
return (
<div className="space-y-8">
{/* PoE Configuration */}
<section>
<h2 className="text-lg font-semibold text-[--color-text] mb-4 flex items-center gap-2 flex-wrap">
Path of Exile
<HelpTip>
PoE Watcher monitors your Client.txt log file to detect zone changes, level ups, and other game events. Your account name is used to fetch character data (equipment, passives, skills) from the public PoE API. Your profile must be set to public at pathofexile.com for snapshots to work.
</HelpTip>
</h2>
<div className="card-inset roundeProvenance: Repobility (https://repobility.com) — every score reproducible from /scan/
ShortcutsTab function · typescript · L1161-L1222 (62 LOC)src/components/Settings/SettingsView.tsx
function ShortcutsTab({
editingHotkeys,
hotkeyErrors,
hotkeyApplyStatus,
hasHotkeyChanges,
hasHotkeyErrors,
handleHotkeyChange,
handleApplyHotkeys,
handleResetHotkeys,
}: ShortcutsTabProps) {
return (
<div className="card-inset rounded-lg p-4 space-y-3">
<div className="flex items-start gap-2 mb-3">
<p className="text-sm text-[--color-text-muted]">
Customize global hotkeys. Click a shortcut to rebind it, then press your desired key combination (must include Ctrl, Shift, or Alt). Press Escape to cancel.
</p>
<HelpTip>
These global hotkeys work even when PoE Watcher is not focused — they're registered system-wide. Click a shortcut field and press your desired key combination. Each shortcut must include at least one modifier key (Ctrl, Shift, or Alt). Press Escape while recording to cancel.
</HelpTip>
</div>
{HOTKEY_ACTIONS.map(({ key, label }) => (
<div key={key} className="flex items-centButton function · typescript · L51-L90 (40 LOC)src/components/Shared/Button.tsx
export function Button({
variant = 'secondary',
size = 'md',
icon: Icon,
loading = false,
children,
disabled,
className = '',
style,
...props
}: ButtonProps) {
const vs = variantStyles[variant];
return (
<button
disabled={disabled || loading}
className={`
inline-flex items-center justify-center gap-2 rounded-lg border
transition-all duration-100 active:scale-95 active:brightness-90
focus-visible:ring-2 focus-visible:ring-[--color-poe-gold] focus-visible:ring-offset-1 focus-visible:ring-offset-[--color-poe-darker]
disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100
hover:brightness-110
${vs.className}
${sizeClasses[size]}
${className}
`}
style={{ ...vs.style, ...style }}
{...props}
>
{loading ? (
<svg className="w-4 h-4 spinner" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5}>
<circle cx="12" cy=CustomSelect function · typescript · L18-L171 (154 LOC)src/components/Shared/CustomSelect.tsx
export function CustomSelect({
value,
onChange,
options,
placeholder = 'Select...',
disabled = false,
className = '',
maxHeight = 280,
}: CustomSelectProps) {
const [isOpen, setIsOpen] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const containerRef = useRef<HTMLDivElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const selectedOption = options.find((o) => o.value === value);
const displayText = selectedOption?.label || placeholder;
// Close on click outside
useEffect(() => {
if (!isOpen) return;
const handleClickOutside = (e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isOpen]);
// Scroll highlighted item into view
useEffect(() => {
if (!isOpen EmptyState function · typescript · L11-L22 (12 LOC)src/components/Shared/EmptyState.tsx
export function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) {
return (
<div className="flex flex-col items-center justify-center py-12 px-6 text-center">
<Icon className="w-12 h-12 text-[--color-text-muted] mb-4" strokeWidth={1.25} />
<h3 className="text-lg font-medium text-[--color-text] mb-1">{title}</h3>
{description && (
<p className="text-sm text-[--color-text-muted] max-w-xs">{description}</p>
)}
{action && <div className="mt-4">{action}</div>}
</div>
);
}HelpTip function · typescript · L8-L35 (28 LOC)src/components/Shared/HelpTip.tsx
export function HelpTip({ children }: HelpTipProps) {
const [open, setOpen] = useState(false);
return (
<>
<button
onClick={(e) => {
e.stopPropagation();
setOpen(!open);
}}
className={`inline-flex items-center justify-center rounded-full transition-colors ${
open
? 'text-[--color-poe-gold]'
: 'text-[--color-text-muted] hover:text-[--color-text]'
}`}
title="Help"
type="button"
>
<HelpCircle className="w-4 h-4" />
</button>
{open && (
<div className="w-full text-xs text-[--color-text-muted] mt-2 p-3 rounded-lg bg-[--color-surface-elevated] border border-[--color-border] leading-relaxed">
{children}
</div>
)}
</>
);
}LoadingSpinner function · typescript · L12-L25 (14 LOC)src/components/Shared/LoadingSpinner.tsx
export function LoadingSpinner({ size = 'md', className = '' }: LoadingSpinnerProps) {
return (
<svg
className={`spinner text-[--color-poe-gold] ${sizeClasses[size]} ${className}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
>
<circle cx="12" cy="12" r="10" strokeOpacity={0.25} />
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
</svg>
);
}RunFilter function · typescript · L29-L196 (168 LOC)src/components/Shared/RunFilter.tsx
export function RunFilter({
filters,
onFiltersChange,
onClear,
showPresetFilter = true,
showReferenceToggle = false,
}: RunFilterProps) {
const [availableLeagues, setAvailableLeagues] = useState<string[]>([]);
const [availableCategories, setAvailableCategories] = useState<string[]>([]);
const [availablePresets, setAvailablePresets] = useState<string[]>([]);
// Load distinct values from existing runs
useEffect(() => {
const loadDistinctValues = async () => {
try {
const runs = await invoke<Run[]>('get_runs');
// Extract unique leagues
const leagues = [...new Set(runs.map((r) => r.league).filter(Boolean))] as string[];
setAvailableLeagues(leagues.sort());
// Extract unique categories
const categories = [...new Set(runs.map((r) => r.category))] as string[];
setAvailableCategories(categories.sort());
// Extract unique presets
const presets = [...new Set(
runs.map((r) => r.breSplitTimeEditor function · typescript · L20-L158 (139 LOC)src/components/Shared/SplitTimeEditor.tsx
export function SplitTimeEditor({
breakpoints,
splitTimes,
setSplitTimes,
bossTimes,
setBossTimes,
townTimes,
setTownTimes,
enabledSplits,
onToggleSplit,
onToggleAll,
allSelected,
}: SplitTimeEditorProps) {
const timeInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
const showCheckboxes = !!enabledSplits;
const isEnabled = (name: string) => !showCheckboxes || enabledSplits!.has(name);
const getActiveBreakpoints = () =>
showCheckboxes ? breakpoints.filter(bp => enabledSplits!.has(bp.name)) : breakpoints;
const focusNextTimeInput = (currentBpName: string) => {
const activeList = getActiveBreakpoints();
const currentIndex = activeList.findIndex(bp => bp.name === currentBpName);
if (currentIndex >= 0 && currentIndex < activeList.length - 1) {
const nextBpName = activeList[currentIndex + 1].name;
timeInputRefs.current[nextBpName]?.focus();
}
};
const makeKeyHandler = (
times: Record<string, string>,
Open data scored by Repobility · https://repobility.com
msToDigits function · typescript · L162-L169 (8 LOC)src/components/Shared/SplitTimeEditor.tsx
export function msToDigits(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const raw = `${hours.toString().padStart(2, '0')}${minutes.toString().padStart(2, '0')}${seconds.toString().padStart(2, '0')}`;
return raw.replace(/^0+/, '') || '0';
}msToShortDigits function · typescript · L171-L177 (7 LOC)src/components/Shared/SplitTimeEditor.tsx
export function msToShortDigits(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const raw = `${minutes.toString().padStart(2, '0')}${seconds.toString().padStart(2, '0')}`;
return raw.replace(/^0+/, '') || '0';
}parseDigitsToMs function · typescript · L179-L186 (8 LOC)src/components/Shared/SplitTimeEditor.tsx
export function parseDigitsToMs(digits: string): number {
if (!digits) return 0;
const padded = digits.padStart(6, '0');
const hours = parseInt(padded.slice(0, 2));
const minutes = parseInt(padded.slice(2, 4));
const seconds = parseInt(padded.slice(4, 6));
return (hours * 3600 + minutes * 60 + seconds) * 1000;
}page 1 / 6next ›