344 lines
11 KiB
TypeScript
344 lines
11 KiB
TypeScript
import { useState, useEffect, useRef } from 'react'
|
|
import { invoke } from '@tauri-apps/api/tauri'
|
|
import {
|
|
Monitor,
|
|
Crosshair,
|
|
Check,
|
|
ChevronRight,
|
|
ChevronLeft,
|
|
RefreshCw,
|
|
AlertCircle,
|
|
Settings2
|
|
} from 'lucide-react'
|
|
|
|
interface CalibrationRegion {
|
|
name: string
|
|
key: string
|
|
description: string
|
|
required: boolean
|
|
detected?: boolean
|
|
}
|
|
|
|
const REGIONS: CalibrationRegion[] = [
|
|
{
|
|
name: 'HP Bar',
|
|
key: 'hp_bar',
|
|
description: 'Health bar at bottom of screen',
|
|
required: true
|
|
},
|
|
{
|
|
name: 'Radar',
|
|
key: 'radar',
|
|
description: 'Mini-map showing location',
|
|
required: true
|
|
},
|
|
{
|
|
name: 'Skill Window',
|
|
key: 'skill_window',
|
|
description: 'Window showing skill gains',
|
|
required: false
|
|
},
|
|
{
|
|
name: 'Mob Name',
|
|
key: 'mob_name',
|
|
description: 'Floating text above creatures',
|
|
required: false
|
|
},
|
|
]
|
|
|
|
export default function SetupWizard() {
|
|
const [step, setStep] = useState(0)
|
|
const [isDetecting, setIsDetecting] = useState(false)
|
|
const [detections, setDetections] = useState<Record<string, boolean>>({})
|
|
const [calibration, setCalibration] = useState<any>(null)
|
|
const [showWizard, setShowWizard] = useState(false)
|
|
|
|
useEffect(() => {
|
|
checkFirstRun()
|
|
}, [])
|
|
|
|
const checkFirstRun = async () => {
|
|
try {
|
|
const settings = await invoke<any>('get_settings')
|
|
if (!settings?.ocr?.calibration) {
|
|
setShowWizard(true)
|
|
}
|
|
} catch (e) {
|
|
setShowWizard(true)
|
|
}
|
|
}
|
|
|
|
const autoDetect = async () => {
|
|
setIsDetecting(true)
|
|
try {
|
|
const detected = await invoke<any>('detect_ui_elements')
|
|
setDetections({
|
|
hp_bar: detected.hp_bar.found,
|
|
radar: detected.radar.found,
|
|
skill_window: detected.skill_window.found,
|
|
})
|
|
} catch (e) {
|
|
console.error('Detection failed:', e)
|
|
}
|
|
setIsDetecting(false)
|
|
}
|
|
|
|
const saveCalibration = async () => {
|
|
try {
|
|
await invoke('set_ocr_calibration', { calibration })
|
|
setShowWizard(false)
|
|
} catch (e) {
|
|
console.error('Save failed:', e)
|
|
}
|
|
}
|
|
|
|
if (!showWizard) return null
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-background/95 backdrop-blur-sm z-50 flex items-center justify-center">
|
|
<div className="w-full max-w-4xl bg-surface rounded-2xl border border-border shadow-2xl">
|
|
{/* Header */}
|
|
<div className="p-6 border-b border-border">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center">
|
|
<Settings2 className="w-6 h-6 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-white">EU-Utility Setup</h2>
|
|
<p className="text-text-muted">Configure screen regions for OCR recognition</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Steps */}
|
|
<div className="p-6">
|
|
{step === 0 && (
|
|
<div className="space-y-6">
|
|
<div className="text-center py-8">
|
|
<Monitor className="w-20 h-20 text-primary mx-auto mb-6" />
|
|
<h3 className="text-xl font-bold text-white mb-3">Welcome to EU-Utility V3</h3>
|
|
<p className="text-text-muted max-w-lg mx-auto">
|
|
This setup wizard will help you configure the application to read
|
|
screen elements from Entropia Universe. This enables features like
|
|
automatic loot tracking, HP monitoring, and skill gain detection.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-surface-light rounded-lg p-4 border border-border">
|
|
<h4 className="font-semibold text-white mb-3">What will be configured:</h4>
|
|
<ul className="space-y-2 text-text-muted">
|
|
<li className="flex items-center gap-2">
|
|
<Check className="w-4 h-4 text-accent" />
|
|
HP bar position and reading
|
|
</li>
|
|
<li className="flex items-center gap-2">
|
|
<Check className="w-4 h-4 text-accent" />
|
|
Radar/mini-map coordinate extraction
|
|
</li>
|
|
<li className="flex items-center gap-2">
|
|
<Check className="w-4 h-4 text-accent" />
|
|
Skill window detection
|
|
</li>
|
|
<li className="flex items-center gap-2">
|
|
<Check className="w-4 h-4 text-accent" />
|
|
Mob name recognition
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{step === 1 && (
|
|
<div className="space-y-6">
|
|
<div className="text-center mb-6">
|
|
<h3 className="text-xl font-bold text-white">Auto-Detect UI Elements</h3>
|
|
<p className="text-text-muted mt-2">
|
|
Make sure Entropia Universe is running and visible on your screen,
|
|
then click detect to automatically find UI elements.
|
|
</p>
|
|
</div>
|
|
|
|
<button
|
|
onClick={autoDetect}
|
|
disabled={isDetecting}
|
|
className="w-full py-4 bg-primary hover:bg-primary-hover disabled:opacity-50 text-white rounded-xl font-medium transition-colors flex items-center justify-center gap-3"
|
|
>
|
|
{isDetecting ? (
|
|
<>
|
|
<RefreshCw className="w-5 h-5 animate-spin" />
|
|
Detecting UI elements...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Crosshair className="w-5 h-5" />
|
|
Auto-Detect Elements
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{REGIONS.map((region) => (
|
|
<div
|
|
key={region.key}
|
|
className={`p-4 rounded-lg border ${
|
|
detections[region.key]
|
|
? 'bg-accent/10 border-accent/30'
|
|
: 'bg-surface-light border-border'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="font-medium text-white">{region.name}</span>
|
|
{detections[region.key] ? (
|
|
<Check className="w-5 h-5 text-accent" />
|
|
) : (
|
|
<AlertCircle className="w-5 h-5 text-text-muted" />
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-text-muted">{region.description}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{step === 2 && (
|
|
<RegionCalibrator
|
|
regions={REGIONS}
|
|
onComplete={(cal) => {
|
|
setCalibration(cal)
|
|
setStep(3)
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{step === 3 && (
|
|
<div className="text-center py-12">
|
|
<div className="w-20 h-20 rounded-full bg-accent/10 flex items-center justify-center mx-auto mb-6">
|
|
<Check className="w-10 h-10 text-accent" />
|
|
</div>
|
|
|
|
<h3 className="text-2xl font-bold text-white mb-3">Setup Complete</h3>
|
|
|
|
<p className="text-text-muted max-w-md mx-auto mb-8">
|
|
EU-Utility is now configured to read screen elements.
|
|
You can adjust these settings anytime in Settings {`->`} OCR.
|
|
</p>
|
|
|
|
<button
|
|
onClick={saveCalibration}
|
|
className="px-8 py-3 bg-primary hover:bg-primary-hover text-white rounded-lg font-medium transition-colors"
|
|
>
|
|
Finish Setup
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<div className="p-6 border-t border-border flex justify-between">
|
|
<button
|
|
onClick={() => setStep(Math.max(0, step - 1))}
|
|
disabled={step === 0}
|
|
className="flex items-center gap-2 px-4 py-2 text-text-muted hover:text-white disabled:opacity-30 transition-colors"
|
|
>
|
|
<ChevronLeft className="w-4 h-4" />
|
|
Back
|
|
</button>
|
|
|
|
<div className="flex gap-2">
|
|
{REGIONS.map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className={`w-2 h-2 rounded-full ${
|
|
i <= step ? 'bg-primary' : 'bg-surface-light'
|
|
}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setStep(Math.min(3, step + 1))}
|
|
disabled={step === 3 || (step === 1 && Object.keys(detections).length === 0)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-primary hover:bg-primary-hover disabled:opacity-30 text-white rounded-lg transition-colors"
|
|
>
|
|
{step === 2 ? 'Complete' : 'Next'}
|
|
<ChevronRight className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface RegionCalibratorProps {
|
|
regions: CalibrationRegion[]
|
|
onComplete: (calibration: any) => void
|
|
}
|
|
|
|
function RegionCalibrator({ regions, onComplete }: RegionCalibratorProps) {
|
|
const [activeRegion, setActiveRegion] = useState(0)
|
|
const [regions_, _setRegions] = useState<Record<string, any>>({})
|
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
|
|
const captureScreen = async () => {
|
|
try {
|
|
const screenshot = await invoke<string>('capture_screen')
|
|
return screenshot
|
|
} catch (e) {
|
|
console.error('Capture failed:', e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<p className="text-center text-text-muted mb-4">
|
|
Click and drag on the screenshot to define each region
|
|
</p>
|
|
|
|
<div className="flex gap-2 mb-4">
|
|
{regions.map((region, i) => (
|
|
<button
|
|
key={region.key}
|
|
onClick={() => setActiveRegion(i)}
|
|
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
activeRegion === i
|
|
? 'bg-primary text-white'
|
|
: regions_[region.key]
|
|
? 'bg-accent/20 text-accent'
|
|
: 'bg-surface-light text-text-muted'
|
|
}`}
|
|
>
|
|
{region.name}
|
|
{regions_[region.key] && <Check className="w-3 h-3 inline ml-1" />}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="relative bg-black rounded-lg overflow-hidden aspect-video">
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="w-full h-full cursor-crosshair"
|
|
/>
|
|
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<button
|
|
onClick={captureScreen}
|
|
className="px-4 py-2 bg-primary/80 hover:bg-primary text-white rounded-lg"
|
|
>
|
|
Capture Screen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => onComplete({ regions: regions_ })}
|
|
disabled={Object.keys(regions_).length === 0}
|
|
className="w-full py-3 bg-accent hover:bg-accent-hover disabled:opacity-50 text-white rounded-lg font-medium transition-colors"
|
|
>
|
|
Save Calibration
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|