mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
feat(setup): Preserve existing setup wizard design
This commit is contained in:
parent
3594b77666
commit
5bac6d7d19
22 changed files with 4209 additions and 0 deletions
1
v3/internal/setupwizard/frontend/dist/assets/index-5h4Dv4JW.css
vendored
Normal file
1
v3/internal/setupwizard/frontend/dist/assets/index-5h4Dv4JW.css
vendored
Normal file
File diff suppressed because one or more lines are too long
48
v3/internal/setupwizard/frontend/dist/assets/index-BYm8n1Ze.js
vendored
Normal file
48
v3/internal/setupwizard/frontend/dist/assets/index-BYm8n1Ze.js
vendored
Normal file
File diff suppressed because one or more lines are too long
16
v3/internal/setupwizard/frontend/dist/index.html
vendored
Normal file
16
v3/internal/setupwizard/frontend/dist/index.html
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Wails Setup Wizard</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<script type="module" crossorigin src="/assets/index-BYm8n1Ze.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-5h4Dv4JW.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
v3/internal/setupwizard/frontend/dist/wails-logo.png
vendored
Executable file
BIN
v3/internal/setupwizard/frontend/dist/wails-logo.png
vendored
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
15
v3/internal/setupwizard/frontend/index.html
Normal file
15
v3/internal/setupwizard/frontend/index.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Wails Setup Wizard</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
2687
v3/internal/setupwizard/frontend/package-lock.json
generated
Normal file
2687
v3/internal/setupwizard/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
v3/internal/setupwizard/frontend/package.json
Normal file
26
v3/internal/setupwizard/frontend/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "wails-setup-wizard",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.23.25",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
6
v3/internal/setupwizard/frontend/postcss.config.js
Normal file
6
v3/internal/setupwizard/frontend/postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
v3/internal/setupwizard/frontend/public/wails-logo.png
Executable file
BIN
v3/internal/setupwizard/frontend/public/wails-logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
327
v3/internal/setupwizard/frontend/src/App.tsx
Normal file
327
v3/internal/setupwizard/frontend/src/App.tsx
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import type { DependencyStatus, SystemInfo } from './types';
|
||||
import { checkDependencies, getState } from './api';
|
||||
import WailsLogo from './components/WailsLogo';
|
||||
|
||||
type CheckState = 'idle' | 'checking' | 'complete';
|
||||
|
||||
interface DependencyWithState extends DependencyStatus {
|
||||
checkState: CheckState;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [dependencies, setDependencies] = useState<DependencyWithState[]>([]);
|
||||
const [system, setSystem] = useState<SystemInfo | null>(null);
|
||||
const [started, setStarted] = useState(false);
|
||||
const [allComplete, setAllComplete] = useState(false);
|
||||
|
||||
// Load system info on mount
|
||||
useEffect(() => {
|
||||
getState().then((s) => {
|
||||
setSystem(s.system);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Start checking after logo animation
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setStarted(true);
|
||||
startDependencyCheck();
|
||||
}, 1500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const startDependencyCheck = async () => {
|
||||
// First get the list of dependencies
|
||||
const deps = await checkDependencies();
|
||||
|
||||
// Initialize all as 'idle'
|
||||
const depsWithState: DependencyWithState[] = deps.map(d => ({
|
||||
...d,
|
||||
checkState: 'idle'
|
||||
}));
|
||||
setDependencies(depsWithState);
|
||||
|
||||
// Animate each dependency check one by one with staggered timing
|
||||
for (let i = 0; i < depsWithState.length; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
setDependencies(prev => prev.map((d, idx) =>
|
||||
idx === i ? { ...d, checkState: 'checking' } : d
|
||||
));
|
||||
|
||||
// Simulate check time (the actual check is already done)
|
||||
await new Promise(resolve => setTimeout(resolve, 400 + Math.random() * 300));
|
||||
|
||||
setDependencies(prev => prev.map((d, idx) =>
|
||||
idx === i ? { ...d, checkState: 'complete' } : d
|
||||
));
|
||||
}
|
||||
|
||||
// All complete
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
setAllComplete(true);
|
||||
};
|
||||
|
||||
const allInstalled = dependencies.length > 0 &&
|
||||
dependencies.every(d => d.installed || !d.required);
|
||||
|
||||
const getStatusIcon = (dep: DependencyWithState) => {
|
||||
if (dep.checkState === 'idle') {
|
||||
return (
|
||||
<div className="w-5 h-5 rounded-full border-2 border-gray-600" />
|
||||
);
|
||||
}
|
||||
|
||||
if (dep.checkState === 'checking') {
|
||||
return (
|
||||
<motion.div
|
||||
className="w-5 h-5 rounded-full border-2 border-red-500 border-t-transparent"
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Complete
|
||||
if (dep.installed) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center"
|
||||
>
|
||||
<motion.svg
|
||||
className="w-3 h-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</motion.svg>
|
||||
</motion.div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
className="w-5 h-5 rounded-full bg-red-500 flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0f0f0f] grid-bg relative overflow-hidden">
|
||||
{/* Radial glow background */}
|
||||
<div className="absolute inset-0 radial-glow pointer-events-none" />
|
||||
|
||||
{/* Main content */}
|
||||
<div className="relative z-10 min-h-screen flex flex-col items-center justify-center px-8">
|
||||
{/* Logo */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||||
>
|
||||
<WailsLogo size={160} />
|
||||
</motion.div>
|
||||
|
||||
{/* Title */}
|
||||
<motion.div
|
||||
className="mt-6 text-center"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.5 }}
|
||||
>
|
||||
<h1 className="text-3xl font-bold text-white">
|
||||
Wails Setup
|
||||
</h1>
|
||||
<p className="text-gray-400 mt-2">
|
||||
{system?.os && `${system.os}/${system.arch}`}
|
||||
{system?.wailsVersion && ` • v${system.wailsVersion}`}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Dependencies section */}
|
||||
<motion.div
|
||||
className="mt-10 w-full max-w-md"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: started ? 1 : 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
className="flex items-center gap-2 mb-4"
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<span className="text-sm font-medium text-gray-400 uppercase tracking-wider">
|
||||
Checking Dependencies
|
||||
</span>
|
||||
{!allComplete && dependencies.length > 0 && (
|
||||
<motion.span
|
||||
className="text-xs text-gray-500"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
({dependencies.filter(d => d.checkState === 'complete').length}/{dependencies.length})
|
||||
</motion.span>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Dependency list */}
|
||||
<div className="space-y-2">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{dependencies.map((dep, index) => (
|
||||
<motion.div
|
||||
key={dep.name}
|
||||
className="flex items-center gap-4 p-4 rounded-xl bg-gray-800/50 border border-gray-700/50"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
delay: index * 0.1,
|
||||
duration: 0.3,
|
||||
ease: 'easeOut'
|
||||
}}
|
||||
layout
|
||||
>
|
||||
{/* Status icon */}
|
||||
<div className="flex-shrink-0">
|
||||
{getStatusIcon(dep)}
|
||||
</div>
|
||||
|
||||
{/* Name and version */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-white font-medium">{dep.name}</span>
|
||||
{dep.checkState === 'complete' && dep.version && (
|
||||
<motion.span
|
||||
className="text-xs text-gray-500 font-mono"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
v{dep.version}
|
||||
</motion.span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Message for missing deps */}
|
||||
{dep.checkState === 'complete' && !dep.installed && dep.message && (
|
||||
<motion.p
|
||||
className="text-xs text-red-400 mt-1 truncate"
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
>
|
||||
{dep.message}
|
||||
</motion.p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Required badge */}
|
||||
{dep.required && dep.checkState === 'complete' && !dep.installed && (
|
||||
<motion.span
|
||||
className="text-xs px-2 py-1 rounded bg-red-500/20 text-red-400"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
>
|
||||
Required
|
||||
</motion.span>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<AnimatePresence>
|
||||
{allComplete && (
|
||||
<motion.div
|
||||
className="mt-6"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
{allInstalled ? (
|
||||
<div className="text-center p-4 rounded-xl bg-green-500/10 border border-green-500/30">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: 'spring', stiffness: 200, damping: 15 }}
|
||||
>
|
||||
<div className="text-3xl mb-2">✓</div>
|
||||
</motion.div>
|
||||
<p className="text-green-400 font-medium">
|
||||
All dependencies installed!
|
||||
</p>
|
||||
<p className="text-gray-400 text-sm mt-1">
|
||||
You're ready to build Wails applications.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center p-4 rounded-xl bg-yellow-500/10 border border-yellow-500/30">
|
||||
<div className="text-3xl mb-2">⚠</div>
|
||||
<p className="text-yellow-400 font-medium">
|
||||
Some dependencies are missing
|
||||
</p>
|
||||
<p className="text-gray-400 text-sm mt-1">
|
||||
Install the missing dependencies to continue.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Close button */}
|
||||
<motion.button
|
||||
className="mt-6 w-full btn-primary"
|
||||
onClick={() => window.close()}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
Close
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
|
||||
{/* Loading dots while fetching deps */}
|
||||
{started && dependencies.length === 0 && (
|
||||
<motion.div
|
||||
className="mt-10 flex space-x-2"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="w-2 h-2 rounded-full bg-red-500/50"
|
||||
animate={{
|
||||
scale: [1, 1.3, 1],
|
||||
opacity: [0.5, 1, 0.5]
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
delay: i * 0.15
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
v3/internal/setupwizard/frontend/src/api.ts
Normal file
60
v3/internal/setupwizard/frontend/src/api.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import type { WizardState, DependencyStatus, DockerStatus, UserConfig, WailsConfig } from './types';
|
||||
|
||||
const API_BASE = '/api';
|
||||
|
||||
export async function getState(): Promise<WizardState> {
|
||||
const response = await fetch(`${API_BASE}/state`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function checkDependencies(): Promise<DependencyStatus[]> {
|
||||
const response = await fetch(`${API_BASE}/dependencies/check`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getDockerStatus(): Promise<DockerStatus> {
|
||||
const response = await fetch(`${API_BASE}/docker/status`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function buildDockerImage(): Promise<{ status: string }> {
|
||||
const response = await fetch(`${API_BASE}/docker/build`, { method: 'POST' });
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function detectConfig(): Promise<Partial<UserConfig>> {
|
||||
const response = await fetch(`${API_BASE}/config/detect`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function saveConfig(config: UserConfig): Promise<{ status: string }> {
|
||||
const response = await fetch(`${API_BASE}/config/save`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function complete(): Promise<{ status: string; duration: string }> {
|
||||
const response = await fetch(`${API_BASE}/complete`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function close(): Promise<void> {
|
||||
await fetch(`${API_BASE}/close`);
|
||||
}
|
||||
|
||||
export async function getWailsConfig(): Promise<WailsConfig | null> {
|
||||
const response = await fetch(`${API_BASE}/wails-config`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function saveWailsConfig(config: WailsConfig): Promise<{ status: string }> {
|
||||
const response = await fetch(`${API_BASE}/wails-config`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
interface WailsLogoProps {
|
||||
className?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export default function WailsLogo({ className = '', size = 240 }: WailsLogoProps) {
|
||||
return (
|
||||
<img
|
||||
src="/wails-logo.png"
|
||||
alt="Wails"
|
||||
width={size}
|
||||
height={size}
|
||||
className={`object-contain ${className}`}
|
||||
style={{
|
||||
filter: 'drop-shadow(0 0 60px rgba(239, 68, 68, 0.4))',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
112
v3/internal/setupwizard/frontend/src/index.css
Normal file
112
v3/internal/setupwizard/frontend/src/index.css
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--wails-red: #ef4444;
|
||||
--wails-red-dark: #dc2626;
|
||||
--wails-red-light: #f87171;
|
||||
--bg-primary: #0f0f0f;
|
||||
--bg-secondary: #1f2937;
|
||||
--bg-tertiary: #374151;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
background: var(--bg-primary);
|
||||
color: white;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Gradient text utility */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #ef4444 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Glass morphism card */
|
||||
.glass-card {
|
||||
background: rgba(31, 41, 55, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(55, 65, 81, 0.5);
|
||||
}
|
||||
|
||||
/* Subtle grid background */
|
||||
.grid-bg {
|
||||
background-image:
|
||||
linear-gradient(rgba(239, 68, 68, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(239, 68, 68, 0.03) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
|
||||
/* Radial glow */
|
||||
.radial-glow {
|
||||
background: radial-gradient(ellipse at center, rgba(239, 68, 68, 0.1) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
/* Button hover effect */
|
||||
.btn-primary {
|
||||
@apply bg-gradient-to-r from-red-500 to-red-600 text-white font-semibold
|
||||
py-3 px-8 rounded-xl shadow-lg transition-all duration-300
|
||||
hover:scale-105 hover:shadow-xl hover:shadow-red-500/30
|
||||
active:scale-95;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-transparent border border-gray-600 text-gray-300 font-medium
|
||||
py-3 px-8 rounded-xl transition-all duration-300
|
||||
hover:border-gray-500 hover:text-white hover:bg-gray-800/50;
|
||||
}
|
||||
|
||||
/* Status badge animations */
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Draw check animation for success */
|
||||
.check-path {
|
||||
stroke-dasharray: 100;
|
||||
stroke-dashoffset: 100;
|
||||
animation: drawCheck 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
10
v3/internal/setupwizard/frontend/src/main.tsx
Normal file
10
v3/internal/setupwizard/frontend/src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
61
v3/internal/setupwizard/frontend/src/types.ts
Normal file
61
v3/internal/setupwizard/frontend/src/types.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
export interface DependencyStatus {
|
||||
name: string;
|
||||
installed: boolean;
|
||||
version?: string;
|
||||
path?: string;
|
||||
status: 'installed' | 'not_installed' | 'needs_update' | 'checking';
|
||||
required: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface DockerStatus {
|
||||
installed: boolean;
|
||||
running: boolean;
|
||||
version?: string;
|
||||
imageBuilt: boolean;
|
||||
imageName: string;
|
||||
pullProgress: number;
|
||||
pullStatus: 'idle' | 'pulling' | 'complete' | 'error';
|
||||
pullError?: string;
|
||||
}
|
||||
|
||||
export interface UserConfig {
|
||||
developerName: string;
|
||||
email: string;
|
||||
defaultFramework: string;
|
||||
projectDirectory: string;
|
||||
editor: string;
|
||||
}
|
||||
|
||||
export interface WailsConfig {
|
||||
info: {
|
||||
companyName: string;
|
||||
productName: string;
|
||||
productIdentifier: string;
|
||||
description: string;
|
||||
copyright: string;
|
||||
comments: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SystemInfo {
|
||||
os: string;
|
||||
arch: string;
|
||||
wailsVersion: string;
|
||||
goVersion: string;
|
||||
homeDir: string;
|
||||
gitName?: string;
|
||||
gitEmail?: string;
|
||||
}
|
||||
|
||||
export interface WizardState {
|
||||
currentStep: number;
|
||||
dependencies: DependencyStatus[];
|
||||
docker: DockerStatus;
|
||||
config: UserConfig;
|
||||
system: SystemInfo;
|
||||
startTime: string;
|
||||
}
|
||||
|
||||
export type Step = 'splash' | 'welcome' | 'dependencies' | 'docker' | 'config' | 'wails-config' | 'complete';
|
||||
56
v3/internal/setupwizard/frontend/tailwind.config.js
Normal file
56
v3/internal/setupwizard/frontend/tailwind.config.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'wails-red': {
|
||||
DEFAULT: '#ef4444',
|
||||
dark: '#dc2626',
|
||||
light: '#f87171',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'sans-serif'],
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.5s ease-out',
|
||||
'slide-up': 'slideUp 0.5s ease-out',
|
||||
'scale-in': 'scaleIn 0.3s ease-out',
|
||||
'pulse-glow': 'pulseGlow 2s ease-in-out infinite',
|
||||
'draw-check': 'drawCheck 0.5s ease-out forwards',
|
||||
'shimmer': 'shimmer 2s linear infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { opacity: '0', transform: 'translateY(20px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
scaleIn: {
|
||||
'0%': { opacity: '0', transform: 'scale(0.9)' },
|
||||
'100%': { opacity: '1', transform: 'scale(1)' },
|
||||
},
|
||||
pulseGlow: {
|
||||
'0%, 100%': { boxShadow: '0 0 20px rgba(239, 68, 68, 0.4)' },
|
||||
'50%': { boxShadow: '0 0 40px rgba(239, 68, 68, 0.6)' },
|
||||
},
|
||||
drawCheck: {
|
||||
'0%': { strokeDashoffset: '100' },
|
||||
'100%': { strokeDashoffset: '0' },
|
||||
},
|
||||
shimmer: {
|
||||
'0%': { backgroundPosition: '-200% 0' },
|
||||
'100%': { backgroundPosition: '200% 0' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
20
v3/internal/setupwizard/frontend/tsconfig.json
Normal file
20
v3/internal/setupwizard/frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
10
v3/internal/setupwizard/frontend/vite.config.ts
Normal file
10
v3/internal/setupwizard/frontend/vite.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
})
|
||||
328
v3/internal/setupwizard/wizard.go
Normal file
328
v3/internal/setupwizard/wizard.go
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
package setupwizard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/browser"
|
||||
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
||||
"github.com/wailsapp/wails/v3/internal/version"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist/*
|
||||
var frontendFS embed.FS
|
||||
|
||||
// DependencyStatus represents the status of a dependency
|
||||
type DependencyStatus struct {
|
||||
Name string `json:"name"`
|
||||
Installed bool `json:"installed"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Status string `json:"status"` // "installed", "not_installed", "needs_update"
|
||||
Required bool `json:"required"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// WailsConfigInfo represents the info section of wails.yaml
|
||||
type WailsConfigInfo struct {
|
||||
CompanyName string `json:"companyName" yaml:"companyName"`
|
||||
ProductName string `json:"productName" yaml:"productName"`
|
||||
ProductIdentifier string `json:"productIdentifier" yaml:"productIdentifier"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
Copyright string `json:"copyright" yaml:"copyright"`
|
||||
Comments string `json:"comments,omitempty" yaml:"comments,omitempty"`
|
||||
Version string `json:"version" yaml:"version"`
|
||||
}
|
||||
|
||||
// WailsConfig represents the wails.yaml configuration
|
||||
type WailsConfig struct {
|
||||
Info WailsConfigInfo `json:"info" yaml:"info"`
|
||||
}
|
||||
|
||||
// SystemInfo contains detected system information
|
||||
type SystemInfo struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
WailsVersion string `json:"wailsVersion"`
|
||||
GoVersion string `json:"goVersion"`
|
||||
HomeDir string `json:"homeDir"`
|
||||
OSName string `json:"osName,omitempty"`
|
||||
OSVersion string `json:"osVersion,omitempty"`
|
||||
}
|
||||
|
||||
// WizardState represents the complete wizard state
|
||||
type WizardState struct {
|
||||
Dependencies []DependencyStatus `json:"dependencies"`
|
||||
System SystemInfo `json:"system"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
}
|
||||
|
||||
// Wizard is the setup wizard server
|
||||
type Wizard struct {
|
||||
server *http.Server
|
||||
state WizardState
|
||||
stateMu sync.RWMutex
|
||||
done chan struct{}
|
||||
shutdown chan struct{}
|
||||
}
|
||||
|
||||
// New creates a new setup wizard
|
||||
func New() *Wizard {
|
||||
return &Wizard{
|
||||
done: make(chan struct{}),
|
||||
shutdown: make(chan struct{}),
|
||||
state: WizardState{
|
||||
StartTime: time.Now(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the wizard and opens it in the browser
|
||||
func (w *Wizard) Run() error {
|
||||
// Initialize system info
|
||||
w.initSystemInfo()
|
||||
|
||||
// Find an available port
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find available port: %w", err)
|
||||
}
|
||||
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
url := fmt.Sprintf("http://127.0.0.1:%d", port)
|
||||
|
||||
// Set up HTTP routes
|
||||
mux := http.NewServeMux()
|
||||
w.setupRoutes(mux)
|
||||
|
||||
w.server = &http.Server{
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
// Start server in goroutine
|
||||
go func() {
|
||||
if err := w.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
fmt.Printf("Setup wizard running at %s\n", url)
|
||||
|
||||
// Open browser
|
||||
if err := browser.OpenURL(url); err != nil {
|
||||
fmt.Printf("Please open %s in your browser\n", url)
|
||||
}
|
||||
|
||||
// Wait for completion or shutdown
|
||||
select {
|
||||
case <-w.done:
|
||||
fmt.Println("\nSetup completed successfully!")
|
||||
case <-w.shutdown:
|
||||
fmt.Println("\nSetup wizard closed.")
|
||||
}
|
||||
|
||||
// Shutdown server
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return w.server.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func (w *Wizard) setupRoutes(mux *http.ServeMux) {
|
||||
// API routes
|
||||
mux.HandleFunc("/api/state", w.handleState)
|
||||
mux.HandleFunc("/api/dependencies/check", w.handleCheckDependencies)
|
||||
mux.HandleFunc("/api/wails-config", w.handleWailsConfig)
|
||||
mux.HandleFunc("/api/complete", w.handleComplete)
|
||||
mux.HandleFunc("/api/close", w.handleClose)
|
||||
|
||||
// Serve frontend
|
||||
frontendDist, err := fs.Sub(frontendFS, "frontend/dist")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fileServer := http.FileServer(http.FS(frontendDist))
|
||||
|
||||
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Try to serve the file
|
||||
path := r.URL.Path
|
||||
if path == "/" {
|
||||
path = "/index.html"
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if _, err := fs.Stat(frontendDist, strings.TrimPrefix(path, "/")); err != nil {
|
||||
// Serve index.html for SPA routing
|
||||
r.URL.Path = "/"
|
||||
}
|
||||
|
||||
fileServer.ServeHTTP(rw, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (w *Wizard) initSystemInfo() {
|
||||
w.stateMu.Lock()
|
||||
defer w.stateMu.Unlock()
|
||||
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
|
||||
w.state.System = SystemInfo{
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
WailsVersion: version.String(),
|
||||
GoVersion: runtime.Version(),
|
||||
HomeDir: homeDir,
|
||||
}
|
||||
|
||||
// Get OS details
|
||||
if info, err := operatingsystem.Info(); err == nil {
|
||||
w.state.System.OSName = info.Name
|
||||
w.state.System.OSVersion = info.Version
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Wizard) handleState(rw http.ResponseWriter, r *http.Request) {
|
||||
w.stateMu.RLock()
|
||||
defer w.stateMu.RUnlock()
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(w.state)
|
||||
}
|
||||
|
||||
func (w *Wizard) handleCheckDependencies(rw http.ResponseWriter, r *http.Request) {
|
||||
deps := w.checkAllDependencies()
|
||||
|
||||
w.stateMu.Lock()
|
||||
w.state.Dependencies = deps
|
||||
w.stateMu.Unlock()
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(deps)
|
||||
}
|
||||
|
||||
func (w *Wizard) handleWailsConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Find wails.yaml in current directory or parent directories
|
||||
configPath := findWailsConfig()
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if configPath == "" {
|
||||
json.NewEncoder(rw).Encode(nil)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
json.NewEncoder(rw).Encode(nil)
|
||||
return
|
||||
}
|
||||
|
||||
var config WailsConfig
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
json.NewEncoder(rw).Encode(nil)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(rw).Encode(config)
|
||||
|
||||
case http.MethodPost:
|
||||
var config WailsConfig
|
||||
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if configPath == "" {
|
||||
configPath = "wails.yaml"
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(rw).Encode(map[string]string{"status": "saved", "path": configPath})
|
||||
|
||||
default:
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func findWailsConfig() string {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for {
|
||||
configPath := filepath.Join(dir, "wails.yaml")
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
return configPath
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *Wizard) handleComplete(rw http.ResponseWriter, r *http.Request) {
|
||||
w.stateMu.RLock()
|
||||
state := w.state
|
||||
w.stateMu.RUnlock()
|
||||
|
||||
duration := time.Since(state.StartTime)
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": "complete",
|
||||
"duration": duration.String(),
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(response)
|
||||
|
||||
close(w.done)
|
||||
}
|
||||
|
||||
func (w *Wizard) handleClose(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(map[string]string{"status": "closing"})
|
||||
|
||||
close(w.shutdown)
|
||||
}
|
||||
|
||||
// execCommand runs a command and returns its output
|
||||
func execCommand(name string, args ...string) (string, error) {
|
||||
cmd := exec.Command(name, args...)
|
||||
output, err := cmd.Output()
|
||||
return strings.TrimSpace(string(output)), err
|
||||
}
|
||||
|
||||
// commandExists checks if a command exists in PATH
|
||||
func commandExists(name string) bool {
|
||||
_, err := exec.LookPath(name)
|
||||
return err == nil
|
||||
}
|
||||
135
v3/internal/setupwizard/wizard_darwin.go
Normal file
135
v3/internal/setupwizard/wizard_darwin.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
//go:build darwin
|
||||
|
||||
package setupwizard
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (w *Wizard) checkAllDependencies() []DependencyStatus {
|
||||
var deps []DependencyStatus
|
||||
|
||||
// Check Xcode Command Line Tools
|
||||
deps = append(deps, checkXcode())
|
||||
|
||||
// Check npm (common dependency)
|
||||
deps = append(deps, checkNpm())
|
||||
|
||||
// Check Docker (optional)
|
||||
deps = append(deps, checkDocker())
|
||||
|
||||
return deps
|
||||
}
|
||||
|
||||
func checkXcode() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "Xcode Command Line Tools",
|
||||
Required: true,
|
||||
}
|
||||
|
||||
path, err := execCommand("xcode-select", "-p")
|
||||
if err != nil {
|
||||
dep.Status = "not_installed"
|
||||
dep.Installed = false
|
||||
dep.Message = "Run: xcode-select --install"
|
||||
return dep
|
||||
}
|
||||
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
|
||||
// Try to get version
|
||||
cmd := exec.Command("pkgutil", "--pkg-info=com.apple.pkg.CLTools_Executables")
|
||||
output, err := cmd.Output()
|
||||
if err == nil {
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "version:") {
|
||||
dep.Version = strings.TrimSpace(strings.TrimPrefix(line, "version:"))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = path // suppress unused warning
|
||||
return dep
|
||||
}
|
||||
|
||||
func checkNpm() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "npm",
|
||||
Required: true,
|
||||
}
|
||||
|
||||
version, err := execCommand("npm", "-v")
|
||||
if err != nil {
|
||||
dep.Status = "not_installed"
|
||||
dep.Installed = false
|
||||
dep.Message = "npm is required. Install Node.js from https://nodejs.org/"
|
||||
return dep
|
||||
}
|
||||
|
||||
dep.Version = version
|
||||
|
||||
// Check minimum version (7.0.0)
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) > 0 {
|
||||
major, _ := strconv.Atoi(parts[0])
|
||||
if major < 7 {
|
||||
dep.Status = "needs_update"
|
||||
dep.Installed = true
|
||||
dep.Message = "npm 7.0.0 or higher is required"
|
||||
return dep
|
||||
}
|
||||
}
|
||||
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
return dep
|
||||
}
|
||||
|
||||
func checkDocker() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "docker",
|
||||
Required: false, // Optional for cross-compilation
|
||||
}
|
||||
|
||||
version, err := execCommand("docker", "--version")
|
||||
if err != nil {
|
||||
dep.Status = "not_installed"
|
||||
dep.Installed = false
|
||||
dep.Message = "Optional - for cross-compilation"
|
||||
return dep
|
||||
}
|
||||
|
||||
// Parse version from "Docker version 24.0.7, build afdd53b"
|
||||
parts := strings.Split(version, ",")
|
||||
if len(parts) > 0 {
|
||||
dep.Version = strings.TrimPrefix(strings.TrimSpace(parts[0]), "Docker version ")
|
||||
}
|
||||
|
||||
// Check if daemon is running
|
||||
_, err = execCommand("docker", "info")
|
||||
if err != nil {
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
dep.Message = "Daemon not running"
|
||||
return dep
|
||||
}
|
||||
|
||||
// Check for wails-cross image
|
||||
imageCheck, _ := execCommand("docker", "image", "inspect", "wails-cross")
|
||||
if imageCheck == "" || strings.Contains(imageCheck, "Error") {
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
dep.Message = "wails-cross image not built"
|
||||
} else {
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
dep.Message = "Cross-compilation ready"
|
||||
}
|
||||
|
||||
return dep
|
||||
}
|
||||
130
v3/internal/setupwizard/wizard_linux.go
Normal file
130
v3/internal/setupwizard/wizard_linux.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
//go:build linux
|
||||
|
||||
package setupwizard
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/doctor/packagemanager"
|
||||
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
||||
)
|
||||
|
||||
func (w *Wizard) checkAllDependencies() []DependencyStatus {
|
||||
var deps []DependencyStatus
|
||||
|
||||
// Get OS info for package manager detection
|
||||
info, _ := operatingsystem.Info()
|
||||
|
||||
// Find the package manager
|
||||
pm := packagemanager.Find(info.ID)
|
||||
if pm != nil {
|
||||
// Get platform dependencies from the doctor package
|
||||
platformDeps, _ := packagemanager.Dependencies(pm)
|
||||
for _, dep := range platformDeps {
|
||||
status := DependencyStatus{
|
||||
Name: dep.Name,
|
||||
Required: !dep.Optional,
|
||||
}
|
||||
|
||||
if dep.Installed {
|
||||
status.Installed = true
|
||||
status.Status = "installed"
|
||||
status.Version = dep.Version
|
||||
} else {
|
||||
status.Installed = false
|
||||
status.Status = "not_installed"
|
||||
if dep.InstallCommand != "" {
|
||||
status.Message = "Install with: " + dep.InstallCommand
|
||||
}
|
||||
}
|
||||
|
||||
deps = append(deps, status)
|
||||
}
|
||||
}
|
||||
|
||||
// Check npm (common dependency)
|
||||
deps = append(deps, checkNpm())
|
||||
|
||||
// Check Docker (optional)
|
||||
deps = append(deps, checkDocker())
|
||||
|
||||
return deps
|
||||
}
|
||||
|
||||
func checkNpm() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "npm",
|
||||
Required: true,
|
||||
}
|
||||
|
||||
version, err := execCommand("npm", "-v")
|
||||
if err != nil {
|
||||
dep.Status = "not_installed"
|
||||
dep.Installed = false
|
||||
dep.Message = "npm is required. Install Node.js from https://nodejs.org/"
|
||||
return dep
|
||||
}
|
||||
|
||||
dep.Version = version
|
||||
|
||||
// Check minimum version (7.0.0)
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) > 0 {
|
||||
major, _ := strconv.Atoi(parts[0])
|
||||
if major < 7 {
|
||||
dep.Status = "needs_update"
|
||||
dep.Installed = true
|
||||
dep.Message = "npm 7.0.0 or higher is required"
|
||||
return dep
|
||||
}
|
||||
}
|
||||
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
return dep
|
||||
}
|
||||
|
||||
func checkDocker() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "docker",
|
||||
Required: false, // Optional for cross-compilation
|
||||
}
|
||||
|
||||
version, err := execCommand("docker", "--version")
|
||||
if err != nil {
|
||||
dep.Status = "not_installed"
|
||||
dep.Installed = false
|
||||
dep.Message = "Optional - for cross-compilation"
|
||||
return dep
|
||||
}
|
||||
|
||||
// Parse version from "Docker version 24.0.7, build afdd53b"
|
||||
parts := strings.Split(version, ",")
|
||||
if len(parts) > 0 {
|
||||
dep.Version = strings.TrimPrefix(strings.TrimSpace(parts[0]), "Docker version ")
|
||||
}
|
||||
|
||||
// Check if daemon is running
|
||||
_, err = execCommand("docker", "info")
|
||||
if err != nil {
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
dep.Message = "Daemon not running"
|
||||
return dep
|
||||
}
|
||||
|
||||
// Check for wails-cross image
|
||||
imageCheck, _ := execCommand("docker", "image", "inspect", "wails-cross")
|
||||
if imageCheck == "" || strings.Contains(imageCheck, "Error") {
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
dep.Message = "wails-cross image not built"
|
||||
} else {
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
dep.Message = "Cross-compilation ready"
|
||||
}
|
||||
|
||||
return dep
|
||||
}
|
||||
142
v3/internal/setupwizard/wizard_windows.go
Normal file
142
v3/internal/setupwizard/wizard_windows.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
//go:build windows
|
||||
|
||||
package setupwizard
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (w *Wizard) checkAllDependencies() []DependencyStatus {
|
||||
var deps []DependencyStatus
|
||||
|
||||
// Check WebView2 Runtime
|
||||
deps = append(deps, checkWebView2())
|
||||
|
||||
// Check npm (common dependency)
|
||||
deps = append(deps, checkNpm())
|
||||
|
||||
// Check Docker (optional)
|
||||
deps = append(deps, checkDocker())
|
||||
|
||||
return deps
|
||||
}
|
||||
|
||||
func checkWebView2() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "WebView2 Runtime",
|
||||
Required: true,
|
||||
}
|
||||
|
||||
// Check common installation paths
|
||||
paths := []string{
|
||||
filepath.Join(os.Getenv("PROGRAMFILES(X86)"), "Microsoft", "EdgeWebView", "Application"),
|
||||
filepath.Join(os.Getenv("LOCALAPPDATA"), "Microsoft", "EdgeWebView", "Application"),
|
||||
filepath.Join(os.Getenv("PROGRAMFILES"), "Microsoft", "EdgeWebView", "Application"),
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
if info, err := os.Stat(path); err == nil && info.IsDir() {
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
|
||||
// Try to get version from directory name
|
||||
entries, _ := os.ReadDir(path)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
name := entry.Name()
|
||||
// Version directories look like "120.0.2210.91"
|
||||
if len(name) > 0 && name[0] >= '0' && name[0] <= '9' {
|
||||
dep.Version = name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return dep
|
||||
}
|
||||
}
|
||||
|
||||
dep.Status = "not_installed"
|
||||
dep.Installed = false
|
||||
dep.Message = "Download from Microsoft Edge WebView2"
|
||||
return dep
|
||||
}
|
||||
|
||||
func checkNpm() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "npm",
|
||||
Required: true,
|
||||
}
|
||||
|
||||
version, err := execCommand("npm", "-v")
|
||||
if err != nil {
|
||||
dep.Status = "not_installed"
|
||||
dep.Installed = false
|
||||
dep.Message = "npm is required. Install Node.js from https://nodejs.org/"
|
||||
return dep
|
||||
}
|
||||
|
||||
dep.Version = version
|
||||
|
||||
// Check minimum version (7.0.0)
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) > 0 {
|
||||
major, _ := strconv.Atoi(parts[0])
|
||||
if major < 7 {
|
||||
dep.Status = "needs_update"
|
||||
dep.Installed = true
|
||||
dep.Message = "npm 7.0.0 or higher is required"
|
||||
return dep
|
||||
}
|
||||
}
|
||||
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
return dep
|
||||
}
|
||||
|
||||
func checkDocker() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "docker",
|
||||
Required: false, // Optional for cross-compilation
|
||||
}
|
||||
|
||||
version, err := execCommand("docker", "--version")
|
||||
if err != nil {
|
||||
dep.Status = "not_installed"
|
||||
dep.Installed = false
|
||||
dep.Message = "Optional - for cross-compilation"
|
||||
return dep
|
||||
}
|
||||
|
||||
// Parse version from "Docker version 24.0.7, build afdd53b"
|
||||
parts := strings.Split(version, ",")
|
||||
if len(parts) > 0 {
|
||||
dep.Version = strings.TrimPrefix(strings.TrimSpace(parts[0]), "Docker version ")
|
||||
}
|
||||
|
||||
// Check if daemon is running
|
||||
_, err = execCommand("docker", "info")
|
||||
if err != nil {
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
dep.Message = "Daemon not running"
|
||||
return dep
|
||||
}
|
||||
|
||||
// Check for wails-cross image
|
||||
imageCheck, _ := execCommand("docker", "image", "inspect", "wails-cross")
|
||||
if imageCheck == "" || strings.Contains(imageCheck, "Error") {
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
dep.Message = "wails-cross image not built"
|
||||
} else {
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
dep.Message = "Cross-compilation ready"
|
||||
}
|
||||
|
||||
return dep
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue