Initial commit: DroidFleet Visor - Fleet management dashboard
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
20
README.md
Normal file
20
README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img width="1200" height="475" alt="GHBanner" src="https://ai.google.dev/static/site-assets/images/share-ais-513315318.png" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# Run and deploy your AI Studio app
|
||||||
|
|
||||||
|
This contains everything you need to run your app locally.
|
||||||
|
|
||||||
|
View your app in AI Studio: https://ai.studio/apps/4a310be8-e39a-4b78-b317-f2fffa484015
|
||||||
|
|
||||||
|
## Run Locally
|
||||||
|
|
||||||
|
**Prerequisites:** Node.js
|
||||||
|
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
`npm install`
|
||||||
|
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||||
|
3. Run the app:
|
||||||
|
`npm run dev`
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>DroidFleet Visor</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤖</text></svg>" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
metadata.json
Normal file
6
metadata.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "Android Hypervisor Cloud",
|
||||||
|
"description": "Cloud virtualization platform for deploying, managing, and renting Android virtual devices with custom bootloaders, root access, and interactive WebRTC/ADB screen streaming.",
|
||||||
|
"requestFramePermissions": [],
|
||||||
|
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
||||||
|
}
|
||||||
4295
package-lock.json
generated
Normal file
4295
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "react-example",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx server.ts",
|
||||||
|
"build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs",
|
||||||
|
"start": "node dist/server.cjs",
|
||||||
|
"lint": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@google/genai": "^2.4.0",
|
||||||
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
"lucide-react": "^0.546.0",
|
||||||
|
"react": "^19.0.1",
|
||||||
|
"react-dom": "^19.0.1",
|
||||||
|
"vite": "^6.2.3",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"motion": "^12.23.24"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.14.0",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
|
"tailwindcss": "^4.1.14",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "~5.8.2",
|
||||||
|
"vite": "^6.2.3",
|
||||||
|
"@types/express": "^4.17.21"
|
||||||
|
}
|
||||||
|
}
|
||||||
364
src/App.tsx
Normal file
364
src/App.tsx
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
AndroidVM,
|
||||||
|
UserSubscription,
|
||||||
|
ProxyConfig,
|
||||||
|
} from './types';
|
||||||
|
import { Navbar, MainNavTab } from './components/Navbar';
|
||||||
|
import { HypervisorConsole } from './components/HypervisorConsole';
|
||||||
|
import { AndroidScreenViewer } from './components/AndroidScreenViewer';
|
||||||
|
import { ProxyManagerStudio } from './components/ProxyManagerStudio';
|
||||||
|
import { McpApiStudio } from './components/McpApiStudio';
|
||||||
|
import { ProgrammaticAutomationStudio } from './components/ProgrammaticAutomationStudio';
|
||||||
|
import { AdbConsole } from './components/AdbConsole';
|
||||||
|
import { BootloaderStudio } from './components/BootloaderStudio';
|
||||||
|
import { PricingModal } from './components/PricingModal';
|
||||||
|
import { DeployVmWizardModal } from './components/DeployVmWizardModal';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [activeTab, setActiveTab] = useState<MainNavTab>('console');
|
||||||
|
const [vms, setVms] = useState<AndroidVM[]>([]);
|
||||||
|
const [selectedVmId, setSelectedVmId] = useState<string>('');
|
||||||
|
const [subscription, setSubscription] = useState<UserSubscription>({
|
||||||
|
plan: 'free',
|
||||||
|
maxVms: 1,
|
||||||
|
pricePerMonth: 0,
|
||||||
|
activeVmCount: 1,
|
||||||
|
renewsAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
const [kvmOnline, setKvmOnline] = useState<boolean>(true);
|
||||||
|
const [isPricingOpen, setIsPricingOpen] = useState(false);
|
||||||
|
const [isDeployWizardOpen, setIsDeployWizardOpen] = useState(false);
|
||||||
|
|
||||||
|
// Fetch initial data from server
|
||||||
|
const fetchAllData = async () => {
|
||||||
|
try {
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
const [subRes, vmsRes, healthRes] = await Promise.all([
|
||||||
|
fetch(`${baseUrl}/api/user/subscription`).catch(() => null),
|
||||||
|
fetch(`${baseUrl}/api/vms`).catch(() => null),
|
||||||
|
fetch(`${baseUrl}/api/health`).catch(() => null),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (subRes && subRes.ok) {
|
||||||
|
const subData = await subRes.json();
|
||||||
|
setSubscription(subData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vmsRes && vmsRes.ok) {
|
||||||
|
const vmsData = await vmsRes.json();
|
||||||
|
setVms(vmsData);
|
||||||
|
if (vmsData.length > 0 && !selectedVmId) {
|
||||||
|
setSelectedVmId(vmsData[0].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (healthRes && healthRes.ok) {
|
||||||
|
setKvmOnline(true);
|
||||||
|
} else {
|
||||||
|
setKvmOnline(false);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setKvmOnline(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAllData();
|
||||||
|
const interval = setInterval(fetchAllData, 3000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const activeVm = vms.find((v) => v.id === selectedVmId) || vms[0];
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const handleDeployVm = async (vmData: any) => {
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
const res = await fetch(`${baseUrl}/api/vms/create`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(vmData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json();
|
||||||
|
throw new Error(err.message || 'Failed to deploy VM');
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdVm = await res.json();
|
||||||
|
setSelectedVmId(createdVm.id);
|
||||||
|
await fetchAllData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePowerAction = async (vmId: string, action: string) => {
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
await fetch(`${baseUrl}/api/vms/${vmId}/power`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ action }),
|
||||||
|
}).catch(() => null);
|
||||||
|
await fetchAllData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteVm = async (vmId: string) => {
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
await fetch(`${baseUrl}/api/vms/${vmId}`, { method: 'DELETE' }).catch(() => null);
|
||||||
|
await fetchAllData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExecuteAdb = async (command: string): Promise<string> => {
|
||||||
|
if (!activeVm) return 'No active VM selected';
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${baseUrl}/api/vms/${activeVm.id}/adb`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ command }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
return data.output || 'Command executed.';
|
||||||
|
} catch {
|
||||||
|
return 'Error executing ADB command on VM backend.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExecuteFastboot = async (command: string): Promise<string> => {
|
||||||
|
if (!activeVm) return 'No active VM selected';
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${baseUrl}/api/vms/${activeVm.id}/fastboot`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ command }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
return data.output || 'OKAY';
|
||||||
|
} catch {
|
||||||
|
return 'Error executing Fastboot command on VM backend.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLaunchApp = async (packageName: string) => {
|
||||||
|
if (!activeVm) return;
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
await fetch(`${baseUrl}/api/vms/${activeVm.id}/launch-app`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ packageName }),
|
||||||
|
}).catch(() => null);
|
||||||
|
await fetchAllData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstallApk = async (appName: string, packageName: string) => {
|
||||||
|
if (!activeVm) return;
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
await fetch(`${baseUrl}/api/vms/${activeVm.id}/install-apk`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ appName, packageName }),
|
||||||
|
}).catch(() => null);
|
||||||
|
await fetchAllData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateBattery = async (level: number, isCharging: boolean) => {
|
||||||
|
if (!activeVm) return;
|
||||||
|
activeVm.batteryLevel = level;
|
||||||
|
activeVm.isCharging = isCharging;
|
||||||
|
setVms([...vms]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateGps = async (latitude: number, longitude: number, addressName: string) => {
|
||||||
|
if (!activeVm) return;
|
||||||
|
activeVm.location = { latitude, longitude, addressName };
|
||||||
|
setVms([...vms]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssignProxyToVm = async (vmId: string, proxy: ProxyConfig) => {
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
await fetch(`${baseUrl}/api/vms/${vmId}/proxy`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(proxy),
|
||||||
|
}).catch(() => null);
|
||||||
|
await fetchAllData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveProxyFromVm = async (vmId: string) => {
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
await fetch(`${baseUrl}/api/vms/${vmId}/proxy`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
}).catch(() => null);
|
||||||
|
await fetchAllData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAutoAssignProxiesToAllVms = async () => {
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
await fetch(`${baseUrl}/api/vms/assign-proxies-auto`, {
|
||||||
|
method: 'POST',
|
||||||
|
}).catch(() => null);
|
||||||
|
await fetchAllData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpgradePlan = async (targetPlan: 'free' | 'pro', paymentMethod?: string) => {
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
const res = await fetch(`${baseUrl}/api/user/subscription/upgrade`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ targetPlan, paymentMethod }),
|
||||||
|
}).catch(() => null);
|
||||||
|
if (res && res.ok) {
|
||||||
|
await fetchAllData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-950 text-slate-100 font-sans flex flex-col justify-between selection:bg-emerald-500 selection:text-white">
|
||||||
|
<div>
|
||||||
|
{/* Navigation Bar */}
|
||||||
|
<Navbar
|
||||||
|
activeTab={activeTab}
|
||||||
|
setActiveTab={setActiveTab}
|
||||||
|
subscription={subscription}
|
||||||
|
onOpenDeployWizard={() => setIsDeployWizardOpen(true)}
|
||||||
|
onOpenPricingModal={() => setIsPricingOpen(true)}
|
||||||
|
kvmOnline={kvmOnline}
|
||||||
|
activeVmName={activeVm?.name}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Main Workspace Workspace */}
|
||||||
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
{activeTab === 'console' && (
|
||||||
|
<HypervisorConsole
|
||||||
|
vms={vms}
|
||||||
|
subscription={subscription}
|
||||||
|
onSelectVm={(id) => {
|
||||||
|
setSelectedVmId(id);
|
||||||
|
setActiveTab('display');
|
||||||
|
}}
|
||||||
|
onOpenDeployWizard={() => setIsDeployWizardOpen(true)}
|
||||||
|
onOpenPricingModal={() => setIsPricingOpen(true)}
|
||||||
|
onPowerAction={handlePowerAction}
|
||||||
|
onDeleteVm={handleDeleteVm}
|
||||||
|
onOpenProxyManager={(vmId) => {
|
||||||
|
if (vmId) setSelectedVmId(vmId);
|
||||||
|
setActiveTab('proxies');
|
||||||
|
}}
|
||||||
|
selectedVmId={selectedVmId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'display' && activeVm && (
|
||||||
|
<AndroidScreenViewer
|
||||||
|
vm={activeVm}
|
||||||
|
vms={vms}
|
||||||
|
onSelectVm={setSelectedVmId}
|
||||||
|
onLaunchApp={handleLaunchApp}
|
||||||
|
onInstallApk={handleInstallApk}
|
||||||
|
onPowerAction={(action) => handlePowerAction(activeVm.id, action)}
|
||||||
|
onUpdateBattery={handleUpdateBattery}
|
||||||
|
onUpdateGps={handleUpdateGps}
|
||||||
|
onOpenProxyManager={(vmId) => {
|
||||||
|
if (vmId) setSelectedVmId(vmId);
|
||||||
|
setActiveTab('proxies');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'proxies' && (
|
||||||
|
<ProxyManagerStudio
|
||||||
|
vms={vms}
|
||||||
|
onAssignProxyToVm={handleAssignProxyToVm}
|
||||||
|
onRemoveProxyFromVm={handleRemoveProxyFromVm}
|
||||||
|
onAutoAssignProxiesToAllVms={handleAutoAssignProxiesToAllVms}
|
||||||
|
onSelectVm={setSelectedVmId}
|
||||||
|
selectedVmId={selectedVmId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'mcp' && (
|
||||||
|
<McpApiStudio vms={vms} onRefreshVms={fetchAllData} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'automation' && activeVm && (
|
||||||
|
<ProgrammaticAutomationStudio
|
||||||
|
vm={activeVm}
|
||||||
|
onRunAdbCommand={async (cmd) => {
|
||||||
|
const out = await handleExecuteAdb(cmd);
|
||||||
|
return { output: out, exitCode: 0 };
|
||||||
|
}}
|
||||||
|
onLaunchApp={handleLaunchApp}
|
||||||
|
onInstallApk={handleInstallApk}
|
||||||
|
inspectorCoords={null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'adb' && activeVm && (
|
||||||
|
<AdbConsole
|
||||||
|
vm={activeVm}
|
||||||
|
vms={vms}
|
||||||
|
onSelectVm={setSelectedVmId}
|
||||||
|
onExecuteAdb={handleExecuteAdb}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'bootloader' && activeVm && (
|
||||||
|
<BootloaderStudio
|
||||||
|
vm={activeVm}
|
||||||
|
vms={vms}
|
||||||
|
onSelectVm={setSelectedVmId}
|
||||||
|
onExecuteFastboot={handleExecuteFastboot}
|
||||||
|
onUpdateBootloaderConfig={(cfg) => {
|
||||||
|
activeVm.bootloader = { ...activeVm.bootloader, ...cfg };
|
||||||
|
setVms([...vms]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'pricing' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6">
|
||||||
|
<h2 className="text-xl font-bold text-white mb-2">Android Cloud Hypervisor Rental</h2>
|
||||||
|
<p className="text-xs text-slate-400 mb-4">
|
||||||
|
Rent virtualized Android devices on demand. Choose 1 Free instance or unlock 5 instances for $5/mo.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsPricingOpen(true)}
|
||||||
|
className="px-5 py-2.5 bg-emerald-600 text-white font-bold text-xs rounded-xl shadow-lg shadow-emerald-600/20"
|
||||||
|
>
|
||||||
|
Manage Subscription & Rental Plans
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modals */}
|
||||||
|
<PricingModal
|
||||||
|
isOpen={isPricingOpen}
|
||||||
|
onClose={() => setIsPricingOpen(false)}
|
||||||
|
subscription={subscription}
|
||||||
|
onUpgradePlan={handleUpgradePlan}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeployVmWizardModal
|
||||||
|
isOpen={isDeployWizardOpen}
|
||||||
|
onClose={() => setIsDeployWizardOpen(false)}
|
||||||
|
subscription={subscription}
|
||||||
|
onDeployVm={handleDeployVm}
|
||||||
|
onOpenPricing={() => {
|
||||||
|
setIsDeployWizardOpen(false);
|
||||||
|
setIsPricingOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="border-t border-slate-900 bg-slate-950/90 py-6 text-center text-xs text-slate-500">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 flex flex-col sm:flex-row items-center justify-between gap-2">
|
||||||
|
<span>Android Cloud Hypervisor • Built-in Bootloader & WebRTC Screen Streaming</span>
|
||||||
|
<span className="text-slate-600 font-mono">1 Free VM • 5 VMs for $5/mo</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
224
src/components/AdbConsole.tsx
Normal file
224
src/components/AdbConsole.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { AndroidVM } from '../types';
|
||||||
|
import {
|
||||||
|
Terminal,
|
||||||
|
Send,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
Trash2,
|
||||||
|
Play,
|
||||||
|
ShieldAlert,
|
||||||
|
Cpu,
|
||||||
|
Layers,
|
||||||
|
Search,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface AdbConsoleProps {
|
||||||
|
vm: AndroidVM;
|
||||||
|
vms: AndroidVM[];
|
||||||
|
onSelectVm: (vmId: string) => void;
|
||||||
|
onExecuteAdb: (command: string) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdbConsole: React.FC<AdbConsoleProps> = ({
|
||||||
|
vm,
|
||||||
|
vms,
|
||||||
|
onSelectVm,
|
||||||
|
onExecuteAdb,
|
||||||
|
}) => {
|
||||||
|
const [commandInput, setCommandInput] = useState('');
|
||||||
|
const [terminalHistory, setTerminalHistory] = useState<
|
||||||
|
{ command: string; output: string; timestamp: string }[]
|
||||||
|
>([
|
||||||
|
{
|
||||||
|
command: 'adb connect 192.168.122.105:5555',
|
||||||
|
output: `* daemon not running; starting now at tcp:5037\n* daemon started successfully\nconnected to ${vm.ipAddress}:${vm.adbPort}`,
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const [isExecuting, setIsExecuting] = useState(false);
|
||||||
|
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
||||||
|
const terminalEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
terminalEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [terminalHistory]);
|
||||||
|
|
||||||
|
const handleRunCommand = async (cmdToRun?: string) => {
|
||||||
|
const cmd = (cmdToRun || commandInput).trim();
|
||||||
|
if (!cmd) return;
|
||||||
|
|
||||||
|
setIsExecuting(true);
|
||||||
|
const result = await onExecuteAdb(cmd);
|
||||||
|
|
||||||
|
setTerminalHistory((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
command: cmd,
|
||||||
|
output: result,
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
setCommandInput('');
|
||||||
|
setIsExecuting(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const presetCommands = [
|
||||||
|
{ label: 'System Build Props', cmd: 'getprop' },
|
||||||
|
{ label: 'List Packages', cmd: 'pm list packages' },
|
||||||
|
{ label: 'Check Root Privilege', cmd: 'su' },
|
||||||
|
{ label: 'CPU Info', cmd: 'cat /proc/cpuinfo' },
|
||||||
|
{ label: 'Battery Dumpsys', cmd: 'dumpsys battery' },
|
||||||
|
{ label: 'Live Logcat', cmd: 'logcat -d' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleCopy = (text: string, index: number) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setCopiedIndex(index);
|
||||||
|
setTimeout(() => setCopiedIndex(null), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Target VM Selector & Network Status Bar */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 bg-slate-900/80 p-4 rounded-2xl border border-slate-800">
|
||||||
|
<div className="flex items-center space-x-3 w-full sm:w-auto">
|
||||||
|
<Terminal className="w-5 h-5 text-emerald-400" />
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-xs font-bold text-slate-300">Target Instance:</span>
|
||||||
|
<select
|
||||||
|
value={vm.id}
|
||||||
|
onChange={(e) => onSelectVm(e.target.value)}
|
||||||
|
className="bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-emerald-400 font-bold focus:outline-none focus:border-emerald-500"
|
||||||
|
>
|
||||||
|
{vms.map((v) => (
|
||||||
|
<option key={v.id} value={v.id}>
|
||||||
|
{v.name} ({v.ipAddress}:{v.adbPort})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3 text-xs font-mono">
|
||||||
|
<span className="px-2.5 py-1 bg-emerald-950/60 border border-emerald-800 text-emerald-300 rounded-lg">
|
||||||
|
ADB TCP: {vm.ipAddress}:{vm.adbPort}
|
||||||
|
</span>
|
||||||
|
<span className="px-2.5 py-1 bg-slate-950 border border-slate-800 text-slate-400 rounded-lg uppercase">
|
||||||
|
SELinux: {vm.bootloader.selinuxMode}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preset Quick Commands */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider block">
|
||||||
|
Quick ADB Shell Presets:
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center space-x-2 overflow-x-auto pb-1">
|
||||||
|
{presetCommands.map((preset) => (
|
||||||
|
<button
|
||||||
|
key={preset.cmd}
|
||||||
|
onClick={() => handleRunCommand(preset.cmd)}
|
||||||
|
disabled={isExecuting}
|
||||||
|
className="px-3 py-1.5 bg-slate-900 hover:bg-slate-800 text-slate-300 border border-slate-800 hover:border-emerald-500/50 rounded-xl text-xs font-mono whitespace-nowrap transition-all flex items-center space-x-1.5 shrink-0"
|
||||||
|
>
|
||||||
|
<Play className="w-3 h-3 text-emerald-400" />
|
||||||
|
<span>{preset.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interactive ADB Shell Terminal View */}
|
||||||
|
<div className="bg-slate-950 border border-slate-800 rounded-2xl overflow-hidden flex flex-col h-[520px] shadow-2xl">
|
||||||
|
{/* Terminal Header */}
|
||||||
|
<div className="bg-slate-900 px-4 py-3 border-b border-slate-800 flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-rose-500/80" />
|
||||||
|
<div className="w-3 h-3 rounded-full bg-amber-500/80" />
|
||||||
|
<div className="w-3 h-3 rounded-full bg-emerald-500/80" />
|
||||||
|
<span className="text-xs font-mono text-slate-400 ml-2">
|
||||||
|
adb shell shell@{vm.profile.id}:/$
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setTerminalHistory([])}
|
||||||
|
className="p-1.5 text-slate-400 hover:text-rose-400 hover:bg-slate-800 rounded-lg text-xs flex items-center space-x-1 transition-colors"
|
||||||
|
title="Clear Terminal Output"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
<span className="hidden sm:inline">Clear</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Console Log Area */}
|
||||||
|
<div className="flex-1 p-4 font-mono text-xs overflow-y-auto space-y-4">
|
||||||
|
{terminalHistory.map((item, idx) => (
|
||||||
|
<div key={idx} className="space-y-1.5 border-b border-slate-900/80 pb-3">
|
||||||
|
<div className="flex items-center justify-between text-slate-400">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-emerald-400">$</span>
|
||||||
|
<span className="font-bold text-slate-200">{item.command}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-[10px] text-slate-600">{item.timestamp}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(item.output, idx)}
|
||||||
|
className="p-1 text-slate-500 hover:text-slate-200"
|
||||||
|
>
|
||||||
|
{copiedIndex === idx ? (
|
||||||
|
<Check className="w-3 h-3 text-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre className="text-slate-300 bg-slate-900/60 p-3 rounded-xl border border-slate-800/80 whitespace-pre-wrap font-mono leading-relaxed overflow-x-auto text-[11px]">
|
||||||
|
{item.output}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div ref={terminalEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Terminal Command Input */}
|
||||||
|
<div className="p-3 bg-slate-900 border-t border-slate-800">
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleRunCommand();
|
||||||
|
}}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<span className="text-emerald-400 font-mono font-bold text-xs">$</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={commandInput}
|
||||||
|
onChange={(e) => setCommandInput(e.target.value)}
|
||||||
|
placeholder="Type adb shell command (e.g. getprop, logcat, su)..."
|
||||||
|
disabled={isExecuting}
|
||||||
|
className="flex-1 bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs font-mono text-slate-100 placeholder-slate-600 focus:outline-none focus:border-emerald-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isExecuting || !commandInput.trim()}
|
||||||
|
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 disabled:bg-slate-800 text-white font-bold rounded-xl text-xs flex items-center space-x-1.5 transition-all"
|
||||||
|
>
|
||||||
|
<Send className="w-3.5 h-3.5" />
|
||||||
|
<span>Run</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
1765
src/components/AndroidScreenViewer.tsx
Normal file
1765
src/components/AndroidScreenViewer.tsx
Normal file
File diff suppressed because it is too large
Load Diff
224
src/components/BootloaderStudio.tsx
Normal file
224
src/components/BootloaderStudio.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { AndroidVM, RootMethod, SELinuxMode } from '../types';
|
||||||
|
import {
|
||||||
|
ShieldAlert,
|
||||||
|
Unlock,
|
||||||
|
Lock,
|
||||||
|
Cpu,
|
||||||
|
Terminal,
|
||||||
|
RotateCcw,
|
||||||
|
Check,
|
||||||
|
Zap,
|
||||||
|
Play,
|
||||||
|
Settings,
|
||||||
|
HardDrive,
|
||||||
|
Copy,
|
||||||
|
Layers,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface BootloaderStudioProps {
|
||||||
|
vm: AndroidVM;
|
||||||
|
vms: AndroidVM[];
|
||||||
|
onSelectVm: (vmId: string) => void;
|
||||||
|
onExecuteFastboot: (command: string) => Promise<string>;
|
||||||
|
onUpdateBootloaderConfig: (updatedBootloader: Partial<AndroidVM['bootloader']>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BootloaderStudio: React.FC<BootloaderStudioProps> = ({
|
||||||
|
vm,
|
||||||
|
vms,
|
||||||
|
onSelectVm,
|
||||||
|
onExecuteFastboot,
|
||||||
|
onUpdateBootloaderConfig,
|
||||||
|
}) => {
|
||||||
|
const [fastbootCmd, setFastbootCmd] = useState('');
|
||||||
|
const [fastbootOutput, setFastbootOutput] = useState<string>('Fastboot CLI ready.');
|
||||||
|
const [kernelCmdlineInput, setKernelCmdlineInput] = useState(vm.bootloader.kernelCmdline);
|
||||||
|
|
||||||
|
const handleRunFastboot = async (cmd?: string) => {
|
||||||
|
const commandToRun = cmd || fastbootCmd;
|
||||||
|
if (!commandToRun) return;
|
||||||
|
const res = await onExecuteFastboot(commandToRun);
|
||||||
|
setFastbootOutput(`$ ${commandToRun}\n${res}`);
|
||||||
|
setFastbootCmd('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveCmdline = () => {
|
||||||
|
onUpdateBootloaderConfig({ kernelCmdline: kernelCmdlineInput });
|
||||||
|
setFastbootOutput(`Updated kernel commandline: ${kernelCmdlineInput}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Target Selector & Bootloader Status Bar */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 bg-slate-900/80 p-4 rounded-2xl border border-slate-800">
|
||||||
|
<div className="flex items-center space-x-3 w-full sm:w-auto">
|
||||||
|
<ShieldAlert className="w-5 h-5 text-amber-400" />
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-xs font-bold text-slate-300">Selected Device:</span>
|
||||||
|
<select
|
||||||
|
value={vm.id}
|
||||||
|
onChange={(e) => onSelectVm(e.target.value)}
|
||||||
|
className="bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-amber-400 font-bold focus:outline-none focus:border-amber-500"
|
||||||
|
>
|
||||||
|
{vms.map((v) => (
|
||||||
|
<option key={v.id} value={v.id}>
|
||||||
|
{v.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span
|
||||||
|
className={`px-3 py-1 rounded-xl text-xs font-bold flex items-center space-x-1.5 ${
|
||||||
|
vm.bootloader.isUnlocked
|
||||||
|
? 'bg-amber-950/60 border border-amber-800 text-amber-300'
|
||||||
|
: 'bg-emerald-950/60 border border-emerald-800 text-emerald-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{vm.bootloader.isUnlocked ? <Unlock className="w-3.5 h-3.5" /> : <Lock className="w-3.5 h-3.5" />}
|
||||||
|
<span>BOOTLOADER {vm.bootloader.isUnlocked ? 'UNLOCKED' : 'LOCKED'}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bootloader & Root Configuration Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{/* Bootloader & Root Controls */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 space-y-4">
|
||||||
|
<h3 className="font-bold text-sm text-slate-200 flex items-center space-x-2">
|
||||||
|
<Unlock className="w-4 h-4 text-amber-400" />
|
||||||
|
<span>Bootloader & Root Configuration</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Toggle Bootloader Unlock */}
|
||||||
|
<div className="p-3 bg-slate-950 rounded-xl border border-slate-800 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="font-bold text-xs text-slate-200 block">Bootloader Lock State</span>
|
||||||
|
<span className="text-[10px] text-slate-400">
|
||||||
|
Allows flashing unsigned boot/recovery partitions
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
onUpdateBootloaderConfig({ isUnlocked: !vm.bootloader.isUnlocked })
|
||||||
|
}
|
||||||
|
className={`px-3 py-1.5 rounded-xl text-xs font-bold transition-all ${
|
||||||
|
vm.bootloader.isUnlocked
|
||||||
|
? 'bg-amber-600 text-white'
|
||||||
|
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{vm.bootloader.isUnlocked ? 'Unlocked' : 'Lock Bootloader'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Root Privilege Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold text-slate-300 block">
|
||||||
|
Root Superuser Provider:
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{(['none', 'magisk', 'kernelsu'] as RootMethod[]).map((r) => (
|
||||||
|
<button
|
||||||
|
key={r}
|
||||||
|
onClick={() => onUpdateBootloaderConfig({ rootMethod: r })}
|
||||||
|
className={`py-2 px-3 rounded-xl text-xs font-bold uppercase transition-all ${
|
||||||
|
vm.bootloader.rootMethod === r
|
||||||
|
? 'bg-emerald-600 text-white shadow-md shadow-emerald-600/20'
|
||||||
|
: 'bg-slate-950 text-slate-400 hover:bg-slate-800 border border-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{r}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SELinux Enforcement Toggle */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold text-slate-300 block">SELinux Mode:</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{(['enforcing', 'permissive'] as SELinuxMode[]).map((m) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => onUpdateBootloaderConfig({ selinuxMode: m })}
|
||||||
|
className={`py-2 px-3 rounded-xl text-xs font-bold uppercase transition-all ${
|
||||||
|
vm.bootloader.selinuxMode === m
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-slate-950 text-slate-400 hover:bg-slate-800 border border-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{m}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kernel Commandline Editor */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 space-y-4">
|
||||||
|
<h3 className="font-bold text-sm text-slate-200 flex items-center space-x-2">
|
||||||
|
<Cpu className="w-4 h-4 text-blue-400" />
|
||||||
|
<span>Kernel Boot Arguments & Parameters</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-400 block mb-1 font-medium">
|
||||||
|
Kernel cmdline parameters
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
value={kernelCmdlineInput}
|
||||||
|
onChange={(e) => setKernelCmdlineInput(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-xs font-mono text-emerald-400 focus:outline-none focus:border-emerald-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSaveCmdline}
|
||||||
|
className="w-full py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded-xl text-xs shadow-md shadow-emerald-600/20"
|
||||||
|
>
|
||||||
|
Save Kernel Parameters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fastboot Interactive Console */}
|
||||||
|
<div className="bg-slate-950 border border-slate-800 rounded-2xl p-5 space-y-3">
|
||||||
|
<h3 className="font-bold text-sm text-slate-200 flex items-center space-x-2">
|
||||||
|
<Terminal className="w-4 h-4 text-amber-400" />
|
||||||
|
<span>Fastboot CLI Interface</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2 overflow-x-auto pb-1">
|
||||||
|
<button
|
||||||
|
onClick={() => handleRunFastboot('fastboot devices')}
|
||||||
|
className="px-3 py-1.5 bg-slate-900 hover:bg-slate-800 text-amber-400 border border-slate-800 rounded-xl text-xs font-mono"
|
||||||
|
>
|
||||||
|
fastboot devices
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRunFastboot('fastboot oem unlock')}
|
||||||
|
className="px-3 py-1.5 bg-slate-900 hover:bg-slate-800 text-amber-400 border border-slate-800 rounded-xl text-xs font-mono"
|
||||||
|
>
|
||||||
|
fastboot oem unlock
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRunFastboot('fastboot flash boot boot.img')}
|
||||||
|
className="px-3 py-1.5 bg-slate-900 hover:bg-slate-800 text-amber-400 border border-slate-800 rounded-xl text-xs font-mono"
|
||||||
|
>
|
||||||
|
fastboot flash boot
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre className="bg-slate-900 p-4 rounded-xl border border-slate-800 font-mono text-xs text-amber-300 min-h-[120px] whitespace-pre-wrap">
|
||||||
|
{fastbootOutput}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
350
src/components/DeployVmWizardModal.tsx
Normal file
350
src/components/DeployVmWizardModal.tsx
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
AndroidVersion,
|
||||||
|
Architecture,
|
||||||
|
RootMethod,
|
||||||
|
SELinuxMode,
|
||||||
|
UserSubscription,
|
||||||
|
} from '../types';
|
||||||
|
import { HardwareProfiles } from '../data/deviceProfiles';
|
||||||
|
import {
|
||||||
|
Smartphone,
|
||||||
|
Cpu,
|
||||||
|
ShieldAlert,
|
||||||
|
Unlock,
|
||||||
|
Check,
|
||||||
|
X,
|
||||||
|
Zap,
|
||||||
|
Plus,
|
||||||
|
ArrowRight,
|
||||||
|
Sparkles,
|
||||||
|
Server,
|
||||||
|
Layers,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface DeployVmWizardModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
subscription: UserSubscription;
|
||||||
|
onDeployVm: (vmData: any) => Promise<void>;
|
||||||
|
onOpenPricing: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeployVmWizardModal: React.FC<DeployVmWizardModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
subscription,
|
||||||
|
onDeployVm,
|
||||||
|
onOpenPricing,
|
||||||
|
}) => {
|
||||||
|
const [vmName, setVmName] = useState('');
|
||||||
|
const [androidVersion, setAndroidVersion] = useState<AndroidVersion>('14 (UpsideDownCake)');
|
||||||
|
const [arch, setArch] = useState<Architecture>('x86_64');
|
||||||
|
const [selectedProfileId, setSelectedProfileId] = useState<string>('pixel-8-pro');
|
||||||
|
const [ramGb, setRamGb] = useState<number>(8);
|
||||||
|
const [vcpu, setVcpu] = useState<number>(4);
|
||||||
|
const [storageGb, setStorageGb] = useState<number>(64);
|
||||||
|
const [bootloaderUnlocked, setBootloaderUnlocked] = useState<boolean>(true);
|
||||||
|
const [rootMethod, setRootMethod] = useState<RootMethod>('magisk');
|
||||||
|
const [selinuxMode, setSelinuxMode] = useState<SELinuxMode>('permissive');
|
||||||
|
const [gpuAcceleration, setGpuAcceleration] = useState<'swiftshader' | 'virgl' | 'angle'>('swiftshader');
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const isAtLimit = subscription.activeVmCount >= subscription.maxVms;
|
||||||
|
|
||||||
|
const handleFormSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (isAtLimit) {
|
||||||
|
setErrorMessage(`Limit reached (${subscription.activeVmCount}/${subscription.maxVms} VMs). Upgrade to Pro ($5/mo for 5 VMs) to deploy more instances!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeploying(true);
|
||||||
|
setErrorMessage(null);
|
||||||
|
try {
|
||||||
|
await onDeployVm({
|
||||||
|
name: vmName,
|
||||||
|
androidVersion,
|
||||||
|
arch,
|
||||||
|
profileId: selectedProfileId,
|
||||||
|
ramGb,
|
||||||
|
vcpu,
|
||||||
|
storageGb,
|
||||||
|
bootloaderUnlocked,
|
||||||
|
rootMethod,
|
||||||
|
selinuxMode,
|
||||||
|
gpuAcceleration,
|
||||||
|
});
|
||||||
|
setIsDeploying(false);
|
||||||
|
onClose();
|
||||||
|
} catch (err: any) {
|
||||||
|
setIsDeploying(false);
|
||||||
|
setErrorMessage(err.message || 'Failed to deploy VM');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const versions: AndroidVersion[] = [
|
||||||
|
'15 (VanillaIceCream)',
|
||||||
|
'14 (UpsideDownCake)',
|
||||||
|
'13 (Tiramisu)',
|
||||||
|
'12 (SnowCone)',
|
||||||
|
'11 (RedVelvetCake)',
|
||||||
|
'10 (Quince)',
|
||||||
|
'9.0 (Pie)',
|
||||||
|
'8.1 (Oreo)',
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/80 backdrop-blur-md p-4 overflow-y-auto">
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-3xl max-w-2xl w-full p-6 sm:p-8 space-y-6 relative shadow-2xl my-8">
|
||||||
|
{/* Close Button */}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-5 right-5 text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-800"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 uppercase tracking-wider">
|
||||||
|
Deployment Wizard
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl sm:text-2xl font-extrabold text-white">
|
||||||
|
Deploy New Android Virtual Device
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-slate-400">
|
||||||
|
Configure AOSP image, hardware resource allocation, bootloader unlock state, and superuser root method.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rental Limit Alert */}
|
||||||
|
{isAtLimit && (
|
||||||
|
<div className="p-4 bg-amber-950/80 border border-amber-800 text-amber-300 rounded-2xl text-xs space-y-2">
|
||||||
|
<div className="flex items-center space-x-2 font-bold text-amber-200">
|
||||||
|
<Zap className="w-4 h-4 fill-current text-amber-400" />
|
||||||
|
<span>Rental Capacity Reached ({subscription.activeVmCount}/{subscription.maxVms} VMs)</span>
|
||||||
|
</div>
|
||||||
|
<p className="leading-relaxed">
|
||||||
|
Your {subscription.plan.toUpperCase()} plan is currently at its maximum capacity of {subscription.maxVms} VM(s). Upgrade to Pro Hypervisor ($5/mo) to unlock up to 5 concurrent Android VM instances!
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onOpenPricing}
|
||||||
|
className="px-4 py-2 bg-gradient-to-r from-emerald-600 to-teal-600 text-white font-bold rounded-xl text-xs shadow-md"
|
||||||
|
>
|
||||||
|
Upgrade to Pro Plan ($5/mo for 5 VMs)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{errorMessage && !isAtLimit && (
|
||||||
|
<div className="p-3 bg-rose-950/80 border border-rose-800 text-rose-300 rounded-xl text-xs font-bold">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleFormSubmit} className="space-y-5">
|
||||||
|
{/* Device Name */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-slate-300 mb-1">
|
||||||
|
Virtual Device Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. Pixel 8 Pro - Android 14 Security Test"
|
||||||
|
value={vmName}
|
||||||
|
onChange={(e) => setVmName(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-slate-100 placeholder-slate-600 focus:outline-none focus:border-emerald-500 font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Android OS Version & Architecture */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-slate-300 mb-1">
|
||||||
|
Android Version (AOSP)
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={androidVersion}
|
||||||
|
onChange={(e) => setAndroidVersion(e.target.value as AndroidVersion)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-xs text-emerald-400 font-bold focus:outline-none focus:border-emerald-500"
|
||||||
|
>
|
||||||
|
{versions.map((ver) => (
|
||||||
|
<option key={ver} value={ver}>
|
||||||
|
Android {ver}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-slate-300 mb-1">
|
||||||
|
CPU Architecture
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={arch}
|
||||||
|
onChange={(e) => setArch(e.target.value as Architecture)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-200 font-bold focus:outline-none focus:border-emerald-500"
|
||||||
|
>
|
||||||
|
<option value="x86_64">x86_64 (Fastest Native Translation)</option>
|
||||||
|
<option value="arm64-v8a">arm64-v8a (Native ARM v8)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hardware Device Profile Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-xs font-bold text-slate-300">
|
||||||
|
Select Hardware Profile
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
|
{HardwareProfiles.ALL.map((prof) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={prof.id}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedProfileId(prof.id);
|
||||||
|
setRamGb(prof.defaultRamGb);
|
||||||
|
setVcpu(prof.defaultVcpu);
|
||||||
|
setStorageGb(prof.defaultStorageGb);
|
||||||
|
}}
|
||||||
|
className={`p-3 rounded-xl border text-left transition-all ${
|
||||||
|
selectedProfileId === prof.id
|
||||||
|
? 'bg-slate-950 border-emerald-500 ring-2 ring-emerald-500/20'
|
||||||
|
: 'bg-slate-950/60 border-slate-800 hover:border-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Smartphone className="w-4 h-4 text-emerald-400 mb-1" />
|
||||||
|
<span className="font-bold text-xs text-slate-200 block truncate">
|
||||||
|
{prof.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-slate-500 block">{prof.screenResolution}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* vCPU & RAM Sliders */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 bg-slate-950/80 p-4 rounded-2xl border border-slate-800">
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 mb-1">
|
||||||
|
RAM Allocation: <span className="text-emerald-400">{ramGb} GB</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="2"
|
||||||
|
max="16"
|
||||||
|
step="2"
|
||||||
|
value={ramGb}
|
||||||
|
onChange={(e) => setRamGb(Number(e.target.value))}
|
||||||
|
className="w-full accent-emerald-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 mb-1">
|
||||||
|
vCPU Cores: <span className="text-emerald-400">{vcpu} Cores</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="8"
|
||||||
|
step="1"
|
||||||
|
value={vcpu}
|
||||||
|
onChange={(e) => setVcpu(Number(e.target.value))}
|
||||||
|
className="w-full accent-emerald-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 mb-1">
|
||||||
|
Storage: <span className="text-emerald-400">{storageGb} GB</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="16"
|
||||||
|
max="256"
|
||||||
|
step="16"
|
||||||
|
value={storageGb}
|
||||||
|
onChange={(e) => setStorageGb(Number(e.target.value))}
|
||||||
|
className="w-full accent-emerald-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bootloader & Root Settings */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||||
|
<div className="p-3 bg-slate-950 rounded-xl border border-slate-800 space-y-1">
|
||||||
|
<span className="text-[10px] text-slate-500 font-bold uppercase block">
|
||||||
|
Bootloader State
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setBootloaderUnlocked(!bootloaderUnlocked)}
|
||||||
|
className={`w-full py-1.5 rounded-lg text-xs font-bold ${
|
||||||
|
bootloaderUnlocked ? 'bg-amber-600 text-white' : 'bg-slate-800 text-slate-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{bootloaderUnlocked ? 'Unlocked' : 'Locked'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-slate-950 rounded-xl border border-slate-800 space-y-1">
|
||||||
|
<span className="text-[10px] text-slate-500 font-bold uppercase block">
|
||||||
|
Root Method
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
value={rootMethod}
|
||||||
|
onChange={(e) => setRootMethod(e.target.value as RootMethod)}
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-lg p-1 text-xs text-amber-400 font-bold"
|
||||||
|
>
|
||||||
|
<option value="magisk">Magisk v27.0</option>
|
||||||
|
<option value="kernelsu">KernelSU</option>
|
||||||
|
<option value="none">None (Unrooted)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-slate-950 rounded-xl border border-slate-800 space-y-1">
|
||||||
|
<span className="text-[10px] text-slate-500 font-bold uppercase block">
|
||||||
|
SELinux Mode
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
value={selinuxMode}
|
||||||
|
onChange={(e) => setSelinuxMode(e.target.value as SELinuxMode)}
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-lg p-1 text-xs text-blue-400 font-bold"
|
||||||
|
>
|
||||||
|
<option value="permissive">Permissive</option>
|
||||||
|
<option value="enforcing">Enforcing</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end space-x-3 pt-2 border-t border-slate-800">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2.5 rounded-xl text-xs font-bold text-slate-400 hover:bg-slate-800 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isDeploying || isAtLimit}
|
||||||
|
className="px-6 py-2.5 rounded-xl text-xs font-bold bg-emerald-600 hover:bg-emerald-500 disabled:bg-slate-800 text-white shadow-lg shadow-emerald-600/25 flex items-center space-x-2 transition-all"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>{isDeploying ? 'Deploying Instance...' : 'Deploy Android VM'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
619
src/components/FacebookAppView.tsx
Normal file
619
src/components/FacebookAppView.tsx
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Share2,
|
||||||
|
ThumbsUp,
|
||||||
|
MessageSquare,
|
||||||
|
Share,
|
||||||
|
Search,
|
||||||
|
Bell,
|
||||||
|
User,
|
||||||
|
Tv,
|
||||||
|
ShoppingBag,
|
||||||
|
Plus,
|
||||||
|
X,
|
||||||
|
Image as ImageIcon,
|
||||||
|
Smile,
|
||||||
|
MapPin,
|
||||||
|
MoreHorizontal,
|
||||||
|
Globe,
|
||||||
|
ExternalLink,
|
||||||
|
Send,
|
||||||
|
Heart,
|
||||||
|
Sparkles,
|
||||||
|
RefreshCw,
|
||||||
|
Users,
|
||||||
|
CheckCircle2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface FacebookAppViewProps {
|
||||||
|
onClose?: () => void;
|
||||||
|
userIp?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Post {
|
||||||
|
id: string;
|
||||||
|
authorName: string;
|
||||||
|
authorAvatar: string;
|
||||||
|
timestamp: string;
|
||||||
|
content: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
likes: number;
|
||||||
|
commentsCount: number;
|
||||||
|
userLiked: boolean;
|
||||||
|
comments: { id: string; author: string; avatar: string; text: string; time: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FacebookAppView: React.FC<FacebookAppViewProps> = ({ onClose, userIp }) => {
|
||||||
|
const [viewMode, setViewMode] = useState<'native' | 'webview'>('native');
|
||||||
|
const [activeTab, setActiveTab] = useState<'feed' | 'friends' | 'watch' | 'marketplace' | 'notifications' | 'menu'>('feed');
|
||||||
|
const [showCreatePost, setShowCreatePost] = useState(false);
|
||||||
|
const [newPostText, setNewPostText] = useState('');
|
||||||
|
const [newPostImage, setNewPostImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Comment states
|
||||||
|
const [activeCommentPostId, setActiveCommentPostId] = useState<string | null>(null);
|
||||||
|
const [commentInput, setCommentInput] = useState('');
|
||||||
|
|
||||||
|
// Reaction state
|
||||||
|
const [reactions, setReactions] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
// Sample initial posts
|
||||||
|
const [posts, setPosts] = useState<Post[]>([
|
||||||
|
{
|
||||||
|
id: 'p1',
|
||||||
|
authorName: 'Google Android Studio',
|
||||||
|
authorAvatar: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=100&auto=format&fit=crop&q=80',
|
||||||
|
timestamp: '2 hrs ago · 🌐',
|
||||||
|
content: '🚀 Google Pixel 8 Pro with Tensor G3 is now live in Cloud Hypervisor Studio! Testing hardware virtualization and full Android 14 stack natively in browser.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1598327105666-5b89351aff97?w=800&auto=format&fit=crop&q=80',
|
||||||
|
likes: 1240,
|
||||||
|
commentsCount: 184,
|
||||||
|
userLiked: false,
|
||||||
|
comments: [
|
||||||
|
{ id: 'c1', author: 'Alex Rivera', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=80&auto=format&fit=crop&q=80', text: 'This virtualized phone runs super smooth!', time: '1h' },
|
||||||
|
{ id: 'c2', author: 'Dev Tech', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=80&auto=format&fit=crop&q=80', text: 'Facebook running inside virtualized Android browser is awesome! 🔥', time: '45m' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p2',
|
||||||
|
authorName: 'TechCrunch Official',
|
||||||
|
authorAvatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=100&auto=format&fit=crop&q=80',
|
||||||
|
timestamp: '5 hrs ago · 🌐',
|
||||||
|
content: 'Breakthrough in WebAssembly & KVM Virtualization allows desktop & mobile operating systems to run inside modern web browsers with near-native hardware acceleration.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=800&auto=format&fit=crop&q=80',
|
||||||
|
likes: 856,
|
||||||
|
commentsCount: 92,
|
||||||
|
userLiked: true,
|
||||||
|
comments: [
|
||||||
|
{ id: 'c3', author: 'Sarah Chen', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=80&auto=format&fit=crop&q=80', text: 'Future of dev environments is right here.', time: '2h' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p3',
|
||||||
|
authorName: 'Cyberpunk Explorers',
|
||||||
|
authorAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100&auto=format&fit=crop&q=80',
|
||||||
|
timestamp: 'Yesterday at 8:15 PM · 🌐',
|
||||||
|
content: 'Night vibes in Tokyo 🇯🇵 Captured on Pixel 8 Pro Night Sight mode.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1503899036084-c55cdd92da26?w=800&auto=format&fit=crop&q=80',
|
||||||
|
likes: 3420,
|
||||||
|
commentsCount: 410,
|
||||||
|
userLiked: false,
|
||||||
|
comments: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleLike = (postId: string, reaction: string = '👍') => {
|
||||||
|
setReactions((prev) => ({ ...prev, [postId]: reaction }));
|
||||||
|
setPosts((prev) =>
|
||||||
|
prev.map((post) => {
|
||||||
|
if (post.id === postId) {
|
||||||
|
const isLiked = !post.userLiked;
|
||||||
|
return {
|
||||||
|
...post,
|
||||||
|
userLiked: isLiked,
|
||||||
|
likes: isLiked ? post.likes + 1 : Math.max(0, post.likes - 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return post;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddComment = (postId: string) => {
|
||||||
|
if (!commentInput.trim()) return;
|
||||||
|
const newComment = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
author: 'Android User',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=80&auto=format&fit=crop&q=80',
|
||||||
|
text: commentInput,
|
||||||
|
time: 'Just now',
|
||||||
|
};
|
||||||
|
|
||||||
|
setPosts((prev) =>
|
||||||
|
prev.map((post) => {
|
||||||
|
if (post.id === postId) {
|
||||||
|
return {
|
||||||
|
...post,
|
||||||
|
commentsCount: post.commentsCount + 1,
|
||||||
|
comments: [...post.comments, newComment],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return post;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setCommentInput('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreatePost = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newPostText.trim() && !newPostImage) return;
|
||||||
|
|
||||||
|
const newPost: Post = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
authorName: 'Android Hypervisor User',
|
||||||
|
authorAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100&auto=format&fit=crop&q=80',
|
||||||
|
timestamp: 'Just now · 🌐',
|
||||||
|
content: newPostText,
|
||||||
|
imageUrl: newPostImage || undefined,
|
||||||
|
likes: 1,
|
||||||
|
commentsCount: 0,
|
||||||
|
userLiked: true,
|
||||||
|
comments: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
setPosts([newPost, ...posts]);
|
||||||
|
setNewPostText('');
|
||||||
|
setNewPostImage(null);
|
||||||
|
setShowCreatePost(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 bg-slate-950 text-slate-100 flex flex-col h-full rounded-2xl overflow-hidden border border-slate-800">
|
||||||
|
{/* Top App Header */}
|
||||||
|
<div className="bg-[#1877F2] px-3 py-2 flex items-center justify-between shadow-md shrink-0">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-7 h-7 bg-white text-[#1877F2] rounded-full flex items-center justify-center font-black text-lg">
|
||||||
|
f
|
||||||
|
</div>
|
||||||
|
<span className="font-extrabold text-base tracking-tight text-white">facebook</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* View Mode Switcher Toggle */}
|
||||||
|
<div className="flex items-center bg-blue-700/60 rounded-full p-0.5 border border-blue-400/30 text-[10px] font-bold">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('native')}
|
||||||
|
className={`px-2 py-0.5 rounded-full transition-all ${
|
||||||
|
viewMode === 'native' ? 'bg-white text-[#1877F2] shadow-sm' : 'text-blue-100 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
App
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('webview')}
|
||||||
|
className={`px-2 py-0.5 rounded-full transition-all ${
|
||||||
|
viewMode === 'webview' ? 'bg-white text-[#1877F2] shadow-sm' : 'text-blue-100 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Web Live
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header Action Icons */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreatePost(true)}
|
||||||
|
className="w-7 h-7 rounded-full bg-blue-600/80 hover:bg-blue-600 flex items-center justify-center text-white"
|
||||||
|
title="Create Post"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-7 h-7 rounded-full bg-blue-600/80 hover:bg-blue-600 flex items-center justify-center text-white relative"
|
||||||
|
title="Search"
|
||||||
|
>
|
||||||
|
<Search className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Mode View */}
|
||||||
|
{viewMode === 'webview' ? (
|
||||||
|
/* LIVE FACEBOOK EMBEDDED WEB VIEW */
|
||||||
|
<div className="flex-1 bg-slate-900 flex flex-col overflow-hidden">
|
||||||
|
<div className="bg-slate-950 px-3 py-1.5 border-b border-slate-800 flex items-center justify-between text-[10px]">
|
||||||
|
<div className="flex items-center space-x-1.5 text-emerald-400 font-mono">
|
||||||
|
<Globe className="w-3 h-3" />
|
||||||
|
<span>https://m.facebook.com</span>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="https://m.facebook.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center space-x-1 px-2 py-0.5 bg-blue-600 hover:bg-blue-500 text-white rounded text-[9px] font-bold"
|
||||||
|
>
|
||||||
|
<span>Open in New Tab</span>
|
||||||
|
<ExternalLink className="w-2.5 h-2.5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 bg-white relative">
|
||||||
|
<iframe
|
||||||
|
src="https://m.facebook.com"
|
||||||
|
title="Facebook Mobile Web"
|
||||||
|
className="w-full h-full border-none"
|
||||||
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* NATIVE FACEBOOK MOBILE APP INTERFACE */
|
||||||
|
<div className="flex-1 flex flex-col bg-slate-950 overflow-hidden">
|
||||||
|
{/* Main Top Navigation Tabs */}
|
||||||
|
<div className="flex items-center justify-around bg-slate-900 border-b border-slate-800 text-slate-400 py-1 shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('feed')}
|
||||||
|
className={`p-2 flex flex-col items-center relative ${
|
||||||
|
activeTab === 'feed' ? 'text-[#1877F2]' : 'hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Globe className="w-5 h-5" />
|
||||||
|
{activeTab === 'feed' && <div className="absolute bottom-0 left-2 right-2 h-0.5 bg-[#1877F2]" />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('friends')}
|
||||||
|
className={`p-2 flex flex-col items-center relative ${
|
||||||
|
activeTab === 'friends' ? 'text-[#1877F2]' : 'hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Users className="w-5 h-5" />
|
||||||
|
{activeTab === 'friends' && <div className="absolute bottom-0 left-2 right-2 h-0.5 bg-[#1877F2]" />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('watch')}
|
||||||
|
className={`p-2 flex flex-col items-center relative ${
|
||||||
|
activeTab === 'watch' ? 'text-[#1877F2]' : 'hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Tv className="w-5 h-5" />
|
||||||
|
{activeTab === 'watch' && <div className="absolute bottom-0 left-2 right-2 h-0.5 bg-[#1877F2]" />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('marketplace')}
|
||||||
|
className={`p-2 flex flex-col items-center relative ${
|
||||||
|
activeTab === 'marketplace' ? 'text-[#1877F2]' : 'hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ShoppingBag className="w-5 h-5" />
|
||||||
|
{activeTab === 'marketplace' && <div className="absolute bottom-0 left-2 right-2 h-0.5 bg-[#1877F2]" />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('notifications')}
|
||||||
|
className={`p-2 flex flex-col items-center relative ${
|
||||||
|
activeTab === 'notifications' ? 'text-[#1877F2]' : 'hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Bell className="w-5 h-5" />
|
||||||
|
{activeTab === 'notifications' && <div className="absolute bottom-0 left-2 right-2 h-0.5 bg-[#1877F2]" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active Tab Screen Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-2 space-y-3">
|
||||||
|
{activeTab === 'feed' && (
|
||||||
|
<>
|
||||||
|
{/* Create Post Prompt Card */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-2.5 flex items-center space-x-2">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=80&auto=format&fit=crop&q=80"
|
||||||
|
alt="Avatar"
|
||||||
|
className="w-8 h-8 rounded-full border border-slate-700"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreatePost(true)}
|
||||||
|
className="flex-1 bg-slate-950 border border-slate-800 hover:border-slate-700 rounded-full px-3 py-1.5 text-left text-[11px] text-slate-400 transition-colors"
|
||||||
|
>
|
||||||
|
What's on your mind?
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreatePost(true)}
|
||||||
|
className="p-1.5 text-emerald-400 hover:bg-slate-800 rounded-lg"
|
||||||
|
>
|
||||||
|
<ImageIcon className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stories Horizontal Tray */}
|
||||||
|
<div className="flex space-x-2 overflow-x-auto pb-1 text-[10px]">
|
||||||
|
{/* Create Story Card */}
|
||||||
|
<div className="min-w-[70px] h-24 bg-slate-900 border border-slate-800 rounded-xl relative overflow-hidden flex flex-col justify-end p-1 text-center group cursor-pointer">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100&auto=format&fit=crop&q=80"
|
||||||
|
className="absolute inset-0 w-full h-full object-cover opacity-60 group-hover:scale-105 transition-transform"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-6 h-6 bg-[#1877F2] text-white rounded-full flex items-center justify-center shadow-lg border border-white">
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
</div>
|
||||||
|
<span className="relative z-10 font-bold text-[9px] text-white drop-shadow">Add Story</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Friend Stories */}
|
||||||
|
{[
|
||||||
|
{ name: 'Android Dev', img: 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?w=200&auto=format&fit=crop&q=80' },
|
||||||
|
{ name: 'Pixel Club', img: 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=200&auto=format&fit=crop&q=80' },
|
||||||
|
{ name: 'Meta AI', img: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=200&auto=format&fit=crop&q=80' },
|
||||||
|
].map((st, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="min-w-[70px] h-24 bg-slate-900 border border-slate-800 rounded-xl relative overflow-hidden flex flex-col justify-end p-1 cursor-pointer group"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={st.img}
|
||||||
|
className="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-1 left-1 w-5 h-5 rounded-full border-2 border-[#1877F2] overflow-hidden">
|
||||||
|
<img src={st.img} className="w-full h-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<span className="relative z-10 font-bold text-[9px] text-white drop-shadow truncate">{st.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* News Feed Posts List */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{posts.map((post) => (
|
||||||
|
<div
|
||||||
|
key={post.id}
|
||||||
|
className="bg-slate-900 border border-slate-800/80 rounded-2xl p-3 space-y-2.5 shadow-sm"
|
||||||
|
>
|
||||||
|
{/* Post Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<img
|
||||||
|
src={post.authorAvatar}
|
||||||
|
alt={post.authorName}
|
||||||
|
className="w-8 h-8 rounded-full border border-slate-700"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<span className="font-bold text-xs text-slate-100">{post.authorName}</span>
|
||||||
|
<CheckCircle2 className="w-3 h-3 text-blue-400 fill-blue-400/20" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[9px] text-slate-400 font-mono">{post.timestamp}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="text-slate-400 hover:text-white p-1">
|
||||||
|
<MoreHorizontal className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Post Content Text */}
|
||||||
|
<p className="text-xs text-slate-200 leading-relaxed whitespace-pre-wrap">{post.content}</p>
|
||||||
|
|
||||||
|
{/* Post Image Attachment */}
|
||||||
|
{post.imageUrl && (
|
||||||
|
<div className="rounded-xl overflow-hidden border border-slate-800 max-h-64 bg-slate-950">
|
||||||
|
<img
|
||||||
|
src={post.imageUrl}
|
||||||
|
alt="Post attachment"
|
||||||
|
className="w-full h-full object-cover hover:scale-102 transition-transform"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Likes & Comments Count Header */}
|
||||||
|
<div className="flex items-center justify-between text-[10px] text-slate-400 pt-1 border-t border-slate-800/60">
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<span className="w-4 h-4 rounded-full bg-[#1877F2] text-white flex items-center justify-center text-[8px] font-bold">
|
||||||
|
👍
|
||||||
|
</span>
|
||||||
|
<span>{post.likes}</span>
|
||||||
|
</div>
|
||||||
|
<span>{post.commentsCount} comments · 12 shares</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Post Reaction Bar */}
|
||||||
|
<div className="flex items-center justify-around border-t border-b border-slate-800/60 py-1 text-slate-400 text-[10px] font-semibold">
|
||||||
|
<button
|
||||||
|
onClick={() => handleLike(post.id)}
|
||||||
|
className={`flex items-center space-x-1 px-3 py-1 rounded-lg hover:bg-slate-800 transition-colors ${
|
||||||
|
post.userLiked ? 'text-[#1877F2] font-bold' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ThumbsUp className="w-3.5 h-3.5" />
|
||||||
|
<span>{post.userLiked ? reactions[post.id] || 'Liked' : 'Like'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setActiveCommentPostId(activeCommentPostId === post.id ? null : post.id)
|
||||||
|
}
|
||||||
|
className="flex items-center space-x-1 px-3 py-1 rounded-lg hover:bg-slate-800 transition-colors"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-3.5 h-3.5" />
|
||||||
|
<span>Comment</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => alert('Post shared to your Facebook feed!')}
|
||||||
|
className="flex items-center space-x-1 px-3 py-1 rounded-lg hover:bg-slate-800 transition-colors"
|
||||||
|
>
|
||||||
|
<Share className="w-3.5 h-3.5" />
|
||||||
|
<span>Share</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comment Box Section */}
|
||||||
|
{activeCommentPostId === post.id && (
|
||||||
|
<div className="space-y-2 pt-1 animate-fade-in">
|
||||||
|
{/* Existing Comments */}
|
||||||
|
<div className="space-y-1.5 max-h-32 overflow-y-auto pr-1">
|
||||||
|
{post.comments.map((c) => (
|
||||||
|
<div key={c.id} className="flex space-x-2 text-[10px]">
|
||||||
|
<img src={c.avatar} className="w-6 h-6 rounded-full" />
|
||||||
|
<div className="bg-slate-950 p-2 rounded-xl border border-slate-800 flex-1">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="font-bold text-slate-200">{c.author}</span>
|
||||||
|
<span className="text-[8px] text-slate-500">{c.time}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-slate-300 mt-0.5">{c.text}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Write Comment Form */}
|
||||||
|
<div className="flex items-center space-x-1.5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={commentInput}
|
||||||
|
onChange={(e) => setCommentInput(e.target.value)}
|
||||||
|
placeholder="Write a comment..."
|
||||||
|
className="flex-1 bg-slate-950 border border-slate-800 rounded-full px-3 py-1 text-[10px] text-slate-200 focus:outline-none focus:border-[#1877F2]"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => handleAddComment(post.id)}
|
||||||
|
className="p-1.5 bg-[#1877F2] text-white rounded-full hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
<Send className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'marketplace' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="font-bold text-sm text-slate-100">Marketplace Nearby</h3>
|
||||||
|
<span className="text-[10px] text-blue-400 font-mono">San Francisco, CA</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{[
|
||||||
|
{ title: 'Pixel 8 Pro 128GB Mint', price: '$520', img: 'https://images.unsplash.com/photo-1598327105666-5b89351aff97?w=300&auto=format&fit=crop&q=80' },
|
||||||
|
{ title: 'Custom Gaming Rig RTX 4080', price: '$1,400', img: 'https://images.unsplash.com/photo-1587202372775-e229f172b9d7?w=300&auto=format&fit=crop&q=80' },
|
||||||
|
{ title: 'Sony WH-1000XM5', price: '$210', img: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&auto=format&fit=crop&q=80' },
|
||||||
|
{ title: 'Electric Scooter Pro', price: '$350', img: 'https://images.unsplash.com/photo-1511919884226-fd3cad34687c?w=300&auto=format&fit=crop&q=80' },
|
||||||
|
].map((item, idx) => (
|
||||||
|
<div key={idx} className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden p-2 space-y-1">
|
||||||
|
<img src={item.img} className="w-full h-24 object-cover rounded-lg" />
|
||||||
|
<p className="font-bold text-xs text-emerald-400">{item.price}</p>
|
||||||
|
<p className="text-[10px] text-slate-200 font-medium truncate">{item.title}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => alert(`Message sent to seller for ${item.title}`)}
|
||||||
|
className="w-full py-1 bg-blue-600 hover:bg-blue-500 text-white rounded text-[9px] font-bold"
|
||||||
|
>
|
||||||
|
Message Seller
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'notifications' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-bold text-sm text-slate-100 mb-2">Notifications</h3>
|
||||||
|
{[
|
||||||
|
{ text: 'Android Developer liked your photo.', time: '10m ago' },
|
||||||
|
{ text: 'Google Cloud Studio posted a new video.', time: '1h ago' },
|
||||||
|
{ text: 'Your post received 15 new reactions.', time: '3h ago' },
|
||||||
|
].map((n, idx) => (
|
||||||
|
<div key={idx} className="p-2.5 bg-slate-900 border border-slate-800 rounded-xl flex items-center space-x-2 text-[10px]">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-blue-600/20 text-[#1877F2] flex items-center justify-center font-bold">
|
||||||
|
f
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-slate-200">{n.text}</p>
|
||||||
|
<span className="text-[8px] text-slate-500 font-mono">{n.time}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'watch' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="font-bold text-sm text-slate-100">Facebook Watch & Reels</h3>
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-2.5 space-y-2">
|
||||||
|
<div className="relative rounded-xl overflow-hidden bg-black h-48 flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=600&auto=format&fit=crop&q=80"
|
||||||
|
className="w-full h-full object-cover opacity-80"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="w-12 h-12 bg-blue-600/90 text-white rounded-full flex items-center justify-center shadow-2xl font-bold cursor-pointer hover:scale-110 transition-transform">
|
||||||
|
▶
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="font-bold text-xs text-slate-200">Building Cloud Hypervisors with WebAssembly & Rust</p>
|
||||||
|
<p className="text-[10px] text-slate-400">124K views · 2 days ago</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal for Creating New Facebook Post */}
|
||||||
|
{showCreatePost && (
|
||||||
|
<div className="absolute inset-0 z-50 bg-slate-950/90 backdrop-blur-md flex items-center justify-center p-3 animate-fade-in">
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-sm p-3 space-y-3 shadow-2xl">
|
||||||
|
<div className="flex justify-between items-center border-b border-slate-800 pb-2">
|
||||||
|
<span className="font-bold text-xs text-slate-100">Create Facebook Post</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreatePost(false)}
|
||||||
|
className="p-1 text-slate-400 hover:text-white"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleCreatePost} className="space-y-3">
|
||||||
|
<textarea
|
||||||
|
value={newPostText}
|
||||||
|
onChange={(e) => setNewPostText(e.target.value)}
|
||||||
|
placeholder="What's on your mind?"
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-2 text-xs text-slate-100 focus:outline-none focus:border-[#1877F2] h-24 resize-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Photo Preset Selector */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] text-slate-400 font-semibold block mb-1">Add Image Attachment</span>
|
||||||
|
<div className="flex space-x-2 overflow-x-auto pb-1">
|
||||||
|
{[
|
||||||
|
'https://images.unsplash.com/photo-1598327105666-5b89351aff97?w=400&auto=format&fit=crop&q=80',
|
||||||
|
'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=400&auto=format&fit=crop&q=80',
|
||||||
|
'https://images.unsplash.com/photo-1503899036084-c55cdd92da26?w=400&auto=format&fit=crop&q=80',
|
||||||
|
].map((url, i) => (
|
||||||
|
<img
|
||||||
|
key={i}
|
||||||
|
src={url}
|
||||||
|
onClick={() => setNewPostImage(newPostImage === url ? null : url)}
|
||||||
|
className={`w-12 h-12 rounded-lg object-cover cursor-pointer border-2 ${
|
||||||
|
newPostImage === url ? 'border-blue-500 scale-105' : 'border-slate-800 opacity-60'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full py-2 bg-[#1877F2] hover:bg-blue-600 text-white rounded-xl text-xs font-bold transition-colors"
|
||||||
|
>
|
||||||
|
Post to Feed
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
356
src/components/GmailAppView.tsx
Normal file
356
src/components/GmailAppView.tsx
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Mail,
|
||||||
|
Search,
|
||||||
|
Plus,
|
||||||
|
ArrowLeft,
|
||||||
|
Trash2,
|
||||||
|
Star,
|
||||||
|
RefreshCw,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
ShieldCheck,
|
||||||
|
ExternalLink,
|
||||||
|
Bot,
|
||||||
|
User,
|
||||||
|
ChevronDown,
|
||||||
|
Inbox,
|
||||||
|
Send,
|
||||||
|
AlertCircle,
|
||||||
|
Tag,
|
||||||
|
CheckCircle2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export interface EmailMessage {
|
||||||
|
id: string;
|
||||||
|
senderName: string;
|
||||||
|
senderEmail: string;
|
||||||
|
recipientEmail: string;
|
||||||
|
subject: string;
|
||||||
|
body: string;
|
||||||
|
verificationCode?: string;
|
||||||
|
timestamp: string;
|
||||||
|
isRead: boolean;
|
||||||
|
isStarred?: boolean;
|
||||||
|
category: 'primary' | 'social' | 'promotions';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GmailAppViewProps {
|
||||||
|
onClose?: () => void;
|
||||||
|
activeGoogleAccount?: string;
|
||||||
|
onSelectGoogleAccount?: (email: string) => void;
|
||||||
|
customInboxMessages?: EmailMessage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_GMAIL_MESSAGES: EmailMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'msg-inst-01',
|
||||||
|
senderName: 'Instagram Security',
|
||||||
|
senderEmail: 'no-reply@instagram.com',
|
||||||
|
recipientEmail: 'alex.dev.cloud@gmail.com',
|
||||||
|
subject: '482-910 is your Instagram security code',
|
||||||
|
body: 'Hi Alex,\n\nSomeone tried to log in or create an Instagram account with your email address. Use the following 6-digit security code to complete verification:\n\nVerification Code: 482910\n\nThis code will expire in 10 minutes. If you did not request this code, please ignore this email.',
|
||||||
|
verificationCode: '482910',
|
||||||
|
timestamp: 'Just now',
|
||||||
|
isRead: false,
|
||||||
|
category: 'social',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-goog-01',
|
||||||
|
senderName: 'Google Account Team',
|
||||||
|
senderEmail: 'no-reply@accounts.google.com',
|
||||||
|
recipientEmail: 'alex.dev.cloud@gmail.com',
|
||||||
|
subject: 'Welcome to your new Android Google Account',
|
||||||
|
body: 'Hi Alex,\n\nYour Google Account alex.dev.cloud@gmail.com is now signed in and synchronized on Google Pixel 8 Pro in Proxifly Cloud Hypervisor.\n\nYou can use this email for Google Sign-In, Play Store downloads, and 2FA app verification codes.',
|
||||||
|
timestamp: '10 mins ago',
|
||||||
|
isRead: true,
|
||||||
|
category: 'primary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-prox-01',
|
||||||
|
senderName: 'Proxifly Hypervisor',
|
||||||
|
senderEmail: 'support@proxifly.dev',
|
||||||
|
recipientEmail: 'alex.dev.cloud@gmail.com',
|
||||||
|
subject: 'Virtual Device Network & SOCKS5 Proxy Ready',
|
||||||
|
body: 'Your VM is assigned Residential SOCKS5 Proxy (US-East). All outgoing email and network traffic routes safely through dedicated IP.',
|
||||||
|
timestamp: '1 hour ago',
|
||||||
|
isRead: true,
|
||||||
|
category: 'primary',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Helper to push emails into window global store for cross-app sync
|
||||||
|
export const sendSimulatedEmail = (email: EmailMessage) => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const existing = (window as any).__PROXIFLY_INBOX_EMAILS__ || [];
|
||||||
|
(window as any).__PROXIFLY_INBOX_EMAILS__ = [email, ...existing];
|
||||||
|
// Trigger custom event for real-time listeners
|
||||||
|
window.dispatchEvent(new CustomEvent('proxifly-email-received', { detail: email }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GmailAppView: React.FC<GmailAppViewProps> = ({
|
||||||
|
onClose,
|
||||||
|
activeGoogleAccount = 'alex.dev.cloud@gmail.com',
|
||||||
|
onSelectGoogleAccount,
|
||||||
|
}) => {
|
||||||
|
const [messages, setMessages] = useState<EmailMessage[]>(() => {
|
||||||
|
if (typeof window !== 'undefined' && (window as any).__PROXIFLY_INBOX_EMAILS__) {
|
||||||
|
return (window as any).__PROXIFLY_INBOX_EMAILS__;
|
||||||
|
}
|
||||||
|
return DEFAULT_GMAIL_MESSAGES;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedMessage, setSelectedMessage] = useState<EmailMessage | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState<'primary' | 'social' | 'promotions'>('primary');
|
||||||
|
const [copiedCode, setCopiedCode] = useState<string | null>(null);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [showAccountSwitcher, setShowAccountSwitcher] = useState(false);
|
||||||
|
const [accountsList, setAccountsList] = useState<string[]>([
|
||||||
|
'alex.dev.cloud@gmail.com',
|
||||||
|
'developer.testing@gmail.com',
|
||||||
|
'bot.automation.proxifly@gmail.com',
|
||||||
|
]);
|
||||||
|
const [currentAccount, setCurrentAccount] = useState<string>(activeGoogleAccount);
|
||||||
|
|
||||||
|
// Sync state with global store and event listener
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
if (!(window as any).__PROXIFLY_INBOX_EMAILS__) {
|
||||||
|
(window as any).__PROXIFLY_INBOX_EMAILS__ = DEFAULT_GMAIL_MESSAGES;
|
||||||
|
}
|
||||||
|
setMessages((window as any).__PROXIFLY_INBOX_EMAILS__);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewEmail = (e: any) => {
|
||||||
|
if (e.detail) {
|
||||||
|
setMessages((prev) => [e.detail, ...prev]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('proxifly-email-received', handleNewEmail);
|
||||||
|
return () => window.removeEventListener('proxifly-email-received', handleNewEmail);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCopyCode = (code: string) => {
|
||||||
|
navigator.clipboard.writeText(code);
|
||||||
|
setCopiedCode(code);
|
||||||
|
setTimeout(() => setCopiedCode(null), 2500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredMessages = messages.filter((m) => {
|
||||||
|
const matchesAccount = m.recipientEmail.toLowerCase() === currentAccount.toLowerCase() || m.recipientEmail.includes('@');
|
||||||
|
const matchesCategory = m.category === activeTab || (activeTab === 'primary' && !m.category);
|
||||||
|
const matchesSearch =
|
||||||
|
m.subject.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
m.senderName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
m.body.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
return matchesAccount && matchesCategory && matchesSearch;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 bg-slate-950 rounded-2xl border border-slate-800 flex flex-col h-full text-slate-100 overflow-hidden font-sans text-xs select-none">
|
||||||
|
{/* Top Header Bar */}
|
||||||
|
<div className="bg-slate-900 border-b border-slate-800 p-2.5 flex items-center justify-between shadow-md">
|
||||||
|
<div className="flex items-center space-x-2 flex-1 mr-2">
|
||||||
|
{selectedMessage ? (
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedMessage(null)}
|
||||||
|
className="p-1 hover:bg-slate-800 rounded-lg text-slate-300"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="p-1.5 rounded-lg bg-rose-500/10 text-rose-400">
|
||||||
|
<Mail className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="font-extrabold text-xs text-slate-100 truncate">
|
||||||
|
{selectedMessage ? 'Email Details' : 'Gmail'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Account Switcher Badge */}
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAccountSwitcher(!showAccountSwitcher)}
|
||||||
|
className="flex items-center space-x-1.5 bg-slate-950 px-2 py-1 rounded-xl border border-slate-800 text-[10px] hover:border-indigo-500/60 transition-all"
|
||||||
|
>
|
||||||
|
<div className="w-4 h-4 rounded-full bg-rose-600 text-white flex items-center justify-center font-bold text-[8px]">
|
||||||
|
{currentAccount[0].toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-slate-300 truncate max-w-[100px]">{currentAccount}</span>
|
||||||
|
<ChevronDown className="w-3 h-3 text-slate-400" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Account Dropdown */}
|
||||||
|
{showAccountSwitcher && (
|
||||||
|
<div className="absolute right-0 top-8 z-50 w-56 bg-slate-900 border border-slate-800 rounded-xl p-2 shadow-2xl space-y-1">
|
||||||
|
<p className="text-[9px] uppercase font-bold text-slate-500 px-2 pt-1">Active Google Accounts</p>
|
||||||
|
{accountsList.map((acc) => (
|
||||||
|
<button
|
||||||
|
key={acc}
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentAccount(acc);
|
||||||
|
if (onSelectGoogleAccount) onSelectGoogleAccount(acc);
|
||||||
|
setShowAccountSwitcher(false);
|
||||||
|
}}
|
||||||
|
className={`w-full text-left p-2 rounded-lg text-[10px] flex items-center justify-between transition-colors ${
|
||||||
|
currentAccount === acc ? 'bg-indigo-600/20 text-indigo-300 font-bold border border-indigo-500/30' : 'hover:bg-slate-800 text-slate-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="truncate">{acc}</span>
|
||||||
|
{currentAccount === acc && <Check className="w-3 h-3 text-indigo-400" />}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Body */}
|
||||||
|
{selectedMessage ? (
|
||||||
|
/* EMAIL READER VIEW */
|
||||||
|
<div className="flex-1 p-3 overflow-y-auto space-y-3 bg-slate-950">
|
||||||
|
<div className="border-b border-slate-800 pb-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="px-2 py-0.5 rounded bg-rose-500/10 text-rose-400 text-[9px] font-bold uppercase border border-rose-500/20">
|
||||||
|
{selectedMessage.category}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-slate-500 font-mono">{selectedMessage.timestamp}</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-sm font-extrabold text-slate-100">{selectedMessage.subject}</h3>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between bg-slate-900 p-2 rounded-xl border border-slate-800 text-[10px]">
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-slate-200">{selectedMessage.senderName} <{selectedMessage.senderEmail}></p>
|
||||||
|
<p className="text-slate-400 font-mono">To: {selectedMessage.recipientEmail}</p>
|
||||||
|
</div>
|
||||||
|
<ShieldCheck className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Verification Code Highlight Card if present */}
|
||||||
|
{selectedMessage.verificationCode && (
|
||||||
|
<div className="bg-gradient-to-r from-indigo-950/80 to-purple-950/80 border border-indigo-500/40 p-3.5 rounded-2xl shadow-xl space-y-2 text-center">
|
||||||
|
<p className="text-[10px] uppercase font-bold tracking-wider text-indigo-300 flex items-center justify-center gap-1">
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-400" />
|
||||||
|
<span>Security Verification Code Detected</span>
|
||||||
|
</p>
|
||||||
|
<div className="text-2xl font-mono font-black text-white tracking-widest bg-slate-950/90 py-2 rounded-xl border border-indigo-500/30">
|
||||||
|
{selectedMessage.verificationCode}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopyCode(selectedMessage.verificationCode!)}
|
||||||
|
className="w-full py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white font-bold rounded-xl text-xs flex items-center justify-center space-x-1.5 shadow-md transition-all"
|
||||||
|
>
|
||||||
|
{copiedCode === selectedMessage.verificationCode ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-3.5 h-3.5 text-emerald-300" />
|
||||||
|
<span>Copied to Clipboard!</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="w-3.5 h-3.5" />
|
||||||
|
<span>Copy Verification Code</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Email Body Content */}
|
||||||
|
<div className="bg-slate-900 p-3 rounded-xl border border-slate-800 text-[11px] text-slate-300 whitespace-pre-wrap leading-relaxed">
|
||||||
|
{selectedMessage.body}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* INBOX LIST VIEW */
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div className="p-2 border-b border-slate-800 bg-slate-900/50">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="w-3.5 h-3.5 absolute left-3 top-2 text-slate-500" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search mail..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-8 pr-3 py-1 text-[11px] text-slate-200 focus:outline-none focus:border-rose-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Tabs */}
|
||||||
|
<div className="flex items-center justify-around bg-slate-950 border-b border-slate-800 text-[10px] font-bold text-slate-400">
|
||||||
|
{[
|
||||||
|
{ id: 'primary', label: 'Primary', icon: <Inbox className="w-3 h-3" /> },
|
||||||
|
{ id: 'social', label: 'Social (IG/FB)', icon: <Tag className="w-3 h-3" /> },
|
||||||
|
{ id: 'promotions', label: 'Promos', icon: <AlertCircle className="w-3 h-3" /> },
|
||||||
|
].map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id as any)}
|
||||||
|
className={`py-2 px-3 flex items-center space-x-1 border-b-2 transition-colors ${
|
||||||
|
activeTab === tab.id ? 'border-rose-500 text-rose-400 font-extrabold' : 'border-transparent hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.icon}
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message Items List */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-2 space-y-2">
|
||||||
|
{filteredMessages.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-slate-500 space-y-2">
|
||||||
|
<Mail className="w-8 h-8 mx-auto opacity-30" />
|
||||||
|
<p className="text-xs">Your inbox is empty for {currentAccount}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredMessages.map((msg) => (
|
||||||
|
<div
|
||||||
|
key={msg.id}
|
||||||
|
onClick={() => {
|
||||||
|
// Mark as read
|
||||||
|
msg.isRead = true;
|
||||||
|
setSelectedMessage(msg);
|
||||||
|
}}
|
||||||
|
className={`p-2.5 rounded-xl border transition-all cursor-pointer ${
|
||||||
|
!msg.isRead
|
||||||
|
? 'bg-slate-900 border-rose-500/30 text-slate-100 font-bold shadow-md'
|
||||||
|
: 'bg-slate-950/60 border-slate-800/80 text-slate-400 hover:bg-slate-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between text-[10px] mb-1">
|
||||||
|
<span className="font-bold text-rose-400 truncate">{msg.senderName}</span>
|
||||||
|
<span className="text-[9px] font-mono text-slate-500">{msg.timestamp}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="font-bold text-xs text-slate-200 truncate">{msg.subject}</p>
|
||||||
|
<p className="text-[10px] text-slate-400 line-clamp-1 mt-0.5">{msg.body}</p>
|
||||||
|
|
||||||
|
{msg.verificationCode && (
|
||||||
|
<div className="mt-2 flex items-center justify-between bg-slate-950 px-2 py-1 rounded-lg border border-indigo-500/30">
|
||||||
|
<span className="text-[9px] text-indigo-300 font-mono font-bold">Code: {msg.verificationCode}</span>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleCopyCode(msg.verificationCode!);
|
||||||
|
}}
|
||||||
|
className="px-2 py-0.5 bg-indigo-600 text-white rounded text-[9px] font-bold hover:bg-indigo-500"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
365
src/components/HypervisorConsole.tsx
Normal file
365
src/components/HypervisorConsole.tsx
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
AndroidVM,
|
||||||
|
UserSubscription,
|
||||||
|
} from '../types';
|
||||||
|
import {
|
||||||
|
Server,
|
||||||
|
Smartphone,
|
||||||
|
Play,
|
||||||
|
RotateCcw,
|
||||||
|
Square,
|
||||||
|
Trash2,
|
||||||
|
Terminal,
|
||||||
|
ShieldAlert,
|
||||||
|
Cpu,
|
||||||
|
HardDrive,
|
||||||
|
Zap,
|
||||||
|
CheckCircle2,
|
||||||
|
Plus,
|
||||||
|
ArrowUpRight,
|
||||||
|
Sparkles,
|
||||||
|
Activity,
|
||||||
|
Layers,
|
||||||
|
Power,
|
||||||
|
RefreshCw,
|
||||||
|
Globe,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface HypervisorConsoleProps {
|
||||||
|
vms: AndroidVM[];
|
||||||
|
subscription: UserSubscription;
|
||||||
|
onSelectVm: (vmId: string) => void;
|
||||||
|
onOpenDeployWizard: () => void;
|
||||||
|
onOpenPricingModal: () => void;
|
||||||
|
onPowerAction: (vmId: string, action: string) => void;
|
||||||
|
onDeleteVm: (vmId: string) => void;
|
||||||
|
onOpenProxyManager?: (vmId?: string) => void;
|
||||||
|
selectedVmId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HypervisorConsole: React.FC<HypervisorConsoleProps> = ({
|
||||||
|
vms,
|
||||||
|
subscription,
|
||||||
|
onSelectVm,
|
||||||
|
onOpenDeployWizard,
|
||||||
|
onOpenPricingModal,
|
||||||
|
onPowerAction,
|
||||||
|
onDeleteVm,
|
||||||
|
onOpenProxyManager,
|
||||||
|
selectedVmId,
|
||||||
|
}) => {
|
||||||
|
const [filterStatus, setFilterStatus] = useState<string>('all');
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
const filteredVms = vms.filter((vm) => {
|
||||||
|
const matchesStatus = filterStatus === 'all' || vm.status === filterStatus;
|
||||||
|
const matchesSearch =
|
||||||
|
vm.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
vm.androidVersion.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
vm.ipAddress.includes(searchQuery);
|
||||||
|
return matchesStatus && matchesSearch;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isFreePlan = subscription.plan === 'free';
|
||||||
|
const isAtLimit = subscription.activeVmCount >= subscription.maxVms;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Top Welcome / Allocation Banner */}
|
||||||
|
<div className="bg-slate-900/80 border border-slate-800 rounded-2xl p-6 relative overflow-hidden backdrop-blur-sm">
|
||||||
|
<div className="absolute -right-16 -top-16 w-64 h-64 bg-emerald-500/10 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 uppercase tracking-wider">
|
||||||
|
Cloud Virtual Device Hypervisor
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-400">Node cluster US-East-1</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl sm:text-2xl font-extrabold text-white">
|
||||||
|
Android VM Management Hub
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-400 text-xs sm:text-sm max-w-2xl">
|
||||||
|
Virtualize AOSP Android 8.1 through 14 with unthrottled hardware acceleration, unlocked bootloaders, Magisk/KernelSU root, and raw ADB bridge endpoints.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rental Tier Callout Card */}
|
||||||
|
<div className="bg-slate-950/80 border border-slate-800 rounded-xl p-4 min-w-[280px] space-y-3">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-slate-400 font-medium">Rental Capacity:</span>
|
||||||
|
<span className="font-bold text-emerald-400 font-mono">
|
||||||
|
{subscription.activeVmCount} / {subscription.maxVms} VMs Active
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Capacity Progress Bar */}
|
||||||
|
<div className="w-full bg-slate-800 h-2 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full transition-all duration-300 ${
|
||||||
|
isAtLimit ? 'bg-amber-500' : 'bg-emerald-500'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(100, (subscription.activeVmCount / subscription.maxVms) * 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFreePlan ? (
|
||||||
|
<div className="space-y-2 pt-1">
|
||||||
|
<p className="text-[11px] text-amber-300 leading-tight">
|
||||||
|
Free Plan includes 1 Instance. Need 5 concurrent Android VMs?
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={onOpenPricingModal}
|
||||||
|
className="w-full py-2 px-3 rounded-lg bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 text-white font-bold text-xs shadow-md shadow-emerald-600/20 flex items-center justify-center space-x-1.5 transition-all"
|
||||||
|
>
|
||||||
|
<Zap className="w-3.5 h-3.5 fill-current" />
|
||||||
|
<span>Upgrade to Pro ($5/mo for 5 VMs)</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-between text-[11px] text-emerald-400 pt-1">
|
||||||
|
<span className="flex items-center space-x-1">
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||||
|
<span>Pro Tier Active ($5/mo)</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={onOpenPricingModal}
|
||||||
|
className="text-slate-400 hover:text-white underline"
|
||||||
|
>
|
||||||
|
Manage Plan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter & Action Controls */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center space-x-2 w-full sm:w-auto">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by device name, Android version or IP..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="bg-slate-900 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-emerald-500 w-full sm:w-64"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center bg-slate-900 p-1 rounded-xl border border-slate-800 text-xs">
|
||||||
|
{['all', 'running', 'fastboot', 'stopped'].map((st) => (
|
||||||
|
<button
|
||||||
|
key={st}
|
||||||
|
onClick={() => setFilterStatus(st)}
|
||||||
|
className={`px-3 py-1 rounded-lg font-medium capitalize transition-all ${
|
||||||
|
filterStatus === st
|
||||||
|
? 'bg-emerald-600 text-white font-semibold'
|
||||||
|
: 'text-slate-400 hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{st}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onOpenDeployWizard}
|
||||||
|
className="w-full sm:w-auto bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-2.5 px-4 rounded-xl text-xs shadow-lg shadow-emerald-600/25 flex items-center justify-center space-x-2 transition-all"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>Deploy New Android Device</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* VM Grid Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||||
|
{filteredVms.map((vm) => {
|
||||||
|
const isSelected = vm.id === selectedVmId;
|
||||||
|
const isRunning = vm.status === 'running';
|
||||||
|
const isFastboot = vm.status === 'fastboot';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={vm.id}
|
||||||
|
className={`bg-slate-900/90 border rounded-2xl p-5 space-y-4 transition-all duration-200 relative group ${
|
||||||
|
isSelected
|
||||||
|
? 'border-emerald-500 shadow-xl shadow-emerald-500/10 bg-slate-900'
|
||||||
|
: 'border-slate-800 hover:border-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Card Top Row */}
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-slate-800 border border-slate-700/80 flex items-center justify-center text-emerald-400 shrink-0">
|
||||||
|
<Smartphone className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-sm text-slate-100 group-hover:text-emerald-400 transition-colors line-clamp-1">
|
||||||
|
{vm.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-slate-400 font-mono mt-0.5">
|
||||||
|
Android {vm.androidVersion.split(' ')[0]} ({vm.arch})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Indicator Pill */}
|
||||||
|
<span
|
||||||
|
className={`px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider border shrink-0 ${
|
||||||
|
isRunning
|
||||||
|
? 'bg-emerald-950/60 border-emerald-700/80 text-emerald-400'
|
||||||
|
: isFastboot
|
||||||
|
? 'bg-amber-950/60 border-amber-700/80 text-amber-400'
|
||||||
|
: 'bg-slate-800 border-slate-700 text-slate-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{vm.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hardware & Bootloader Spec Tags */}
|
||||||
|
<div className="grid grid-cols-3 gap-2 py-2 border-y border-slate-800/80 text-center">
|
||||||
|
<div className="bg-slate-950/60 p-2 rounded-xl border border-slate-800">
|
||||||
|
<span className="block text-[10px] text-slate-500 uppercase font-medium">
|
||||||
|
RAM / vCPU
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-bold text-slate-200 font-mono">
|
||||||
|
{vm.ramGb}GB / {vm.vcpu}c
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-950/60 p-2 rounded-xl border border-slate-800">
|
||||||
|
<span className="block text-[10px] text-slate-500 uppercase font-medium">
|
||||||
|
Bootloader
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-bold text-emerald-400 font-mono">
|
||||||
|
{vm.bootloader.isUnlocked ? 'Unlocked' : 'Locked'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-950/60 p-2 rounded-xl border border-slate-800">
|
||||||
|
<span className="block text-[10px] text-slate-500 uppercase font-medium">
|
||||||
|
Root
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-bold text-amber-400 uppercase font-mono">
|
||||||
|
{vm.bootloader.rootMethod}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Network, ADB IP & Active Proxy Status */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between text-xs font-mono text-slate-400 bg-slate-950/80 px-3 py-2 rounded-xl border border-slate-800">
|
||||||
|
<span>ADB: {vm.ipAddress}:{vm.adbPort}</span>
|
||||||
|
<span className="text-[10px] text-emerald-400">
|
||||||
|
{vm.metrics.fps} FPS
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => onOpenProxyManager && onOpenProxyManager(vm.id)}
|
||||||
|
className="flex items-center justify-between px-3 py-1.5 rounded-xl border bg-slate-950/60 text-xs font-mono cursor-pointer hover:border-indigo-500/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-1.5 overflow-hidden">
|
||||||
|
<Globe className="w-3.5 h-3.5 text-indigo-400 shrink-0" />
|
||||||
|
{vm.proxy && vm.proxy.enabled ? (
|
||||||
|
<span className="text-slate-200 font-bold truncate">
|
||||||
|
{vm.proxy.type}://{vm.proxy.host}:{vm.proxy.port}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-slate-500 text-[11px]">Direct Connection (No proxy)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{vm.proxy && vm.proxy.enabled ? (
|
||||||
|
<span className="px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-400 font-bold text-[9px] uppercase border border-emerald-500/20 shrink-0">
|
||||||
|
{vm.proxy.countryCode || 'US'}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-indigo-400 text-[10px] font-bold hover:underline shrink-0">
|
||||||
|
+ Proxy
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex items-center justify-between gap-2 pt-1">
|
||||||
|
<button
|
||||||
|
onClick={() => onSelectVm(vm.id)}
|
||||||
|
className="flex-1 bg-emerald-600/20 hover:bg-emerald-600 text-emerald-300 hover:text-white font-semibold py-2 px-3 rounded-xl text-xs border border-emerald-500/30 transition-all flex items-center justify-center space-x-1.5"
|
||||||
|
>
|
||||||
|
<Smartphone className="w-3.5 h-3.5" />
|
||||||
|
<span>Open Screen</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onOpenProxyManager && onOpenProxyManager(vm.id)}
|
||||||
|
className="p-2 bg-slate-800 hover:bg-indigo-600/30 text-indigo-300 rounded-xl transition-colors border border-transparent hover:border-indigo-500/40"
|
||||||
|
title="Manage Proxy for this VM"
|
||||||
|
>
|
||||||
|
<Globe className="w-4 h-4 text-indigo-400" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Power Toggle Buttons */}
|
||||||
|
{isRunning ? (
|
||||||
|
<button
|
||||||
|
onClick={() => onPowerAction(vm.id, 'stop')}
|
||||||
|
className="p-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-xl transition-colors"
|
||||||
|
title="Stop VM"
|
||||||
|
>
|
||||||
|
<Square className="w-4 h-4 text-rose-400" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => onPowerAction(vm.id, 'start')}
|
||||||
|
className="p-2 bg-slate-800 hover:bg-emerald-700 text-slate-300 rounded-xl transition-colors"
|
||||||
|
title="Start VM"
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4 text-emerald-400" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onPowerAction(vm.id, 'reboot')}
|
||||||
|
className="p-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-xl transition-colors"
|
||||||
|
title="Reboot VM"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4 text-blue-400" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onDeleteVm(vm.id)}
|
||||||
|
className="p-2 bg-slate-800 hover:bg-rose-900/50 text-slate-400 hover:text-rose-400 rounded-xl transition-colors"
|
||||||
|
title="Terminate Instance"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredVms.length === 0 && (
|
||||||
|
<div className="bg-slate-900/60 border border-slate-800 rounded-2xl p-12 text-center space-y-4">
|
||||||
|
<Server className="w-12 h-12 text-slate-600 mx-auto" />
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h3 className="text-base font-bold text-slate-200">No Android VMs Deployed</h3>
|
||||||
|
<p className="text-xs text-slate-400 max-w-sm mx-auto">
|
||||||
|
Click the button below to deploy your first virtual Android instance with custom hardware specs and bootloader settings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onOpenDeployWizard}
|
||||||
|
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-2.5 px-5 rounded-xl text-xs shadow-lg shadow-emerald-600/25 inline-flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>Deploy Android VM</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
1046
src/components/InstagramAppView.tsx
Normal file
1046
src/components/InstagramAppView.tsx
Normal file
File diff suppressed because it is too large
Load Diff
618
src/components/McpApiStudio.tsx
Normal file
618
src/components/McpApiStudio.tsx
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { ApiKey, AndroidVM } from '../types';
|
||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
Key,
|
||||||
|
Terminal,
|
||||||
|
Code2,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
RefreshCw,
|
||||||
|
Sparkles,
|
||||||
|
Play,
|
||||||
|
Zap,
|
||||||
|
Globe,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
ShieldCheck,
|
||||||
|
ExternalLink,
|
||||||
|
Layers,
|
||||||
|
ChevronRight,
|
||||||
|
Database,
|
||||||
|
Cpu,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface McpApiStudioProps {
|
||||||
|
vms: AndroidVM[];
|
||||||
|
onRefreshVms: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const McpApiStudio: React.FC<McpApiStudioProps> = ({ vms, onRefreshVms }) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<'mcp' | 'keys' | 'docs' | 'tester'>('mcp');
|
||||||
|
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [copiedText, setCopiedText] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// New key form state
|
||||||
|
const [newKeyName, setNewKeyName] = useState('');
|
||||||
|
const [createdKeySecret, setCreatedKeySecret] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// MCP Tester state
|
||||||
|
const [selectedTool, setSelectedTool] = useState<string>('list_vms');
|
||||||
|
const [testerArgs, setTesterArgs] = useState<string>('{}');
|
||||||
|
const [testerResponse, setTesterResponse] = useState<string | null>(null);
|
||||||
|
const [testingTool, setTestingTool] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchApiKeys();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchApiKeys = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/keys');
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.keys) setApiKeys(data.keys);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch API keys', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateApiKey = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newKeyName.trim()) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/keys', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: newKeyName, permissions: 'full_control' }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.apiKey) {
|
||||||
|
setCreatedKeySecret(data.apiKey.key);
|
||||||
|
setNewKeyName('');
|
||||||
|
await fetchApiKeys();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to create key', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRevokeApiKey = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await fetch(`/api/keys/${id}`, { method: 'DELETE' });
|
||||||
|
await fetchApiKeys();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to revoke key', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = (text: string, label: string) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setCopiedText(label);
|
||||||
|
setTimeout(() => setCopiedText(null), 2500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestMcpTool = async () => {
|
||||||
|
setTestingTool(true);
|
||||||
|
setTesterResponse(null);
|
||||||
|
let parsedArgs = {};
|
||||||
|
try {
|
||||||
|
parsedArgs = JSON.parse(testerArgs);
|
||||||
|
} catch (e) {
|
||||||
|
setTesterResponse('Invalid JSON arguments syntax!');
|
||||||
|
setTestingTool(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/mcp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: Date.now(),
|
||||||
|
method: 'tools/call',
|
||||||
|
params: {
|
||||||
|
name: selectedTool,
|
||||||
|
arguments: parsedArgs,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
setTesterResponse(JSON.stringify(data, null, 2));
|
||||||
|
await onRefreshVms();
|
||||||
|
} catch (err: any) {
|
||||||
|
setTesterResponse(`Error: ${err?.message || 'Failed to execute MCP tool call'}`);
|
||||||
|
} finally {
|
||||||
|
setTestingTool(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentHost = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000';
|
||||||
|
const masterKey = apiKeys[0]?.key || 'sk_proxifly_live_mcp_master_key_8842';
|
||||||
|
|
||||||
|
// Config snippet templates
|
||||||
|
const claudeConfig = JSON.stringify(
|
||||||
|
{
|
||||||
|
mcpServers: {
|
||||||
|
'proxifly-android-hypervisor': {
|
||||||
|
command: 'npx',
|
||||||
|
args: ['-y', '@modelcontextprotocol/server-fetch', `${currentHost}/api/mcp`],
|
||||||
|
env: {
|
||||||
|
PROXIFLY_API_KEY: masterKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
const cursorConfig = JSON.stringify(
|
||||||
|
{
|
||||||
|
mcpServers: [
|
||||||
|
{
|
||||||
|
name: 'Proxifly Android Hypervisor AI Control',
|
||||||
|
url: `${currentHost}/api/mcp`,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${masterKey}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
const pythonCode = `import requests
|
||||||
|
|
||||||
|
# Model Context Protocol (MCP) JSON-RPC 2.0 Client Call
|
||||||
|
url = "${currentHost}/api/mcp"
|
||||||
|
headers = {"Authorization": "Bearer ${masterKey}", "Content-Type": "application/json"}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "execute_adb_command",
|
||||||
|
"arguments": {
|
||||||
|
"vmId": "${vms[0]?.id || 'vm-cloud-001'}",
|
||||||
|
"command": "input tap 500 800"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, json=payload, headers=headers)
|
||||||
|
print(response.json())`;
|
||||||
|
|
||||||
|
const nodeCode = `import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
const response = await fetch('${currentHost}/api/mcp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ${masterKey}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1,
|
||||||
|
method: 'tools/call',
|
||||||
|
params: {
|
||||||
|
name: 'assign_proxy',
|
||||||
|
arguments: {
|
||||||
|
vmId: '${vms[0]?.id || 'vm-cloud-001'}',
|
||||||
|
host: '104.28.19.42',
|
||||||
|
port: 1080,
|
||||||
|
type: 'SOCKS5'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data);`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Top Banner */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 relative overflow-hidden backdrop-blur-sm shadow-2xl">
|
||||||
|
<div className="absolute -right-20 -top-20 w-80 h-80 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30 uppercase tracking-wider flex items-center space-x-1">
|
||||||
|
<Bot className="w-3.5 h-3.5 text-indigo-400" />
|
||||||
|
<span>Model Context Protocol (MCP) Server Active</span>
|
||||||
|
</span>
|
||||||
|
<span className="px-2 py-0.5 rounded bg-emerald-500/20 text-emerald-300 text-[10px] font-mono font-bold">
|
||||||
|
JSON-RPC 2.0 & SSE
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl sm:text-2xl font-extrabold text-white flex items-center space-x-2">
|
||||||
|
<span>Programmatic AI Agent Control Hub</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-400 text-xs sm:text-sm max-w-2xl">
|
||||||
|
Connect Claude, Cursor, ChatGPT, or custom LLM agents via Model Context Protocol to auto-provision VMs, execute ADB taps, sideload APKs, run macros, and assign Proxifly proxies.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Stats */}
|
||||||
|
<div className="flex items-center space-x-3 shrink-0">
|
||||||
|
<div className="bg-slate-950 border border-slate-800 px-4 py-2.5 rounded-xl text-center">
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold">Exposed MCP Tools</p>
|
||||||
|
<p className="text-lg font-mono font-extrabold text-indigo-400">9 Tools</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-950 border border-slate-800 px-4 py-2.5 rounded-xl text-center">
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold">Active API Keys</p>
|
||||||
|
<p className="text-lg font-mono font-extrabold text-emerald-400">{apiKeys.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sub Navigation Tabs */}
|
||||||
|
<div className="flex items-center gap-2 border-t border-slate-800/80 pt-4 mt-6 overflow-x-auto no-scrollbar">
|
||||||
|
{[
|
||||||
|
{ id: 'mcp', label: '1-Click Agent Integration (Claude / Cursor)', icon: <Bot className="w-4 h-4" /> },
|
||||||
|
{ id: 'tester', label: 'Interactive MCP Tool Tester', icon: <Play className="w-4 h-4 text-emerald-400" /> },
|
||||||
|
{ id: 'keys', label: 'API Keys & Secrets', icon: <Key className="w-4 h-4 text-amber-400" /> },
|
||||||
|
{ id: 'docs', label: 'REST API & OpenAPI Specs', icon: <Code2 className="w-4 h-4 text-indigo-400" /> },
|
||||||
|
].map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id as any)}
|
||||||
|
className={`px-3.5 py-2 rounded-xl text-xs font-bold flex items-center space-x-2 transition-all shrink-0 ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'bg-indigo-600 text-white shadow-md'
|
||||||
|
: 'bg-slate-950/60 hover:bg-slate-800 text-slate-400 hover:text-slate-200 border border-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.icon}
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab 1: MCP Integration Configurations */}
|
||||||
|
{activeTab === 'mcp' && (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Claude Desktop MCP Setup */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 space-y-4 shadow-xl relative">
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="p-2 rounded-xl bg-indigo-500/10 text-indigo-400">
|
||||||
|
<Bot className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-extrabold text-sm text-slate-100">Claude Desktop MCP Configuration</h3>
|
||||||
|
<p className="text-[11px] text-slate-400">Add to your claude_desktop_config.json</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(claudeConfig, 'claude')}
|
||||||
|
className="px-3 py-1.5 bg-slate-950 border border-slate-800 hover:border-indigo-500 text-slate-200 rounded-xl text-xs flex items-center space-x-1.5 transition-all"
|
||||||
|
>
|
||||||
|
{copiedText === 'claude' ? <Check className="w-3.5 h-3.5 text-emerald-400" /> : <Copy className="w-3.5 h-3.5 text-indigo-400" />}
|
||||||
|
<span>{copiedText === 'claude' ? 'Copied Config!' : 'Copy Config'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre className="bg-slate-950 border border-slate-800 p-4 rounded-xl text-[11px] font-mono text-indigo-300 overflow-x-auto leading-relaxed">
|
||||||
|
{claudeConfig}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<div className="text-[11px] text-slate-400 space-y-1 bg-slate-950/60 p-3 rounded-xl border border-slate-800/60">
|
||||||
|
<p className="font-bold text-slate-300">📁 Configuration File Paths:</p>
|
||||||
|
<p>• macOS: <code className="text-indigo-400">~/Library/Application Support/Claude/claude_desktop_config.json</code></p>
|
||||||
|
<p>• Windows: <code className="text-indigo-400">%APPDATA%\Claude\claude_desktop_config.json</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cursor AI MCP Setup */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 space-y-4 shadow-xl">
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="p-2 rounded-xl bg-emerald-500/10 text-emerald-400">
|
||||||
|
<Sparkles className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-extrabold text-sm text-slate-100">Cursor IDE & Agent Configuration</h3>
|
||||||
|
<p className="text-[11px] text-slate-400">Add to .cursor/mcp.json or Cursor Features Settings</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(cursorConfig, 'cursor')}
|
||||||
|
className="px-3 py-1.5 bg-slate-950 border border-slate-800 hover:border-emerald-500 text-slate-200 rounded-xl text-xs flex items-center space-x-1.5 transition-all"
|
||||||
|
>
|
||||||
|
{copiedText === 'cursor' ? <Check className="w-3.5 h-3.5 text-emerald-400" /> : <Copy className="w-3.5 h-3.5 text-emerald-400" />}
|
||||||
|
<span>{copiedText === 'cursor' ? 'Copied!' : 'Copy JSON'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre className="bg-slate-950 border border-slate-800 p-4 rounded-xl text-[11px] font-mono text-emerald-300 overflow-x-auto leading-relaxed">
|
||||||
|
{cursorConfig}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<div className="text-[11px] text-slate-400 space-y-1 bg-slate-950/60 p-3 rounded-xl border border-slate-800/60">
|
||||||
|
<p className="font-bold text-slate-300">💡 How to activate in Cursor:</p>
|
||||||
|
<p>1. Open Cursor Settings → Features → MCP Servers</p>
|
||||||
|
<p>2. Add New MCP Server: <code className="text-emerald-400">{currentHost}/api/mcp</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Python SDK Snippet */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 space-y-3 shadow-xl">
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-800 pb-2">
|
||||||
|
<h4 className="font-bold text-xs text-slate-200 flex items-center space-x-2">
|
||||||
|
<Terminal className="w-4 h-4 text-amber-400" />
|
||||||
|
<span>Python LangChain / AutoGen Client Script</span>
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(pythonCode, 'py')}
|
||||||
|
className="text-[11px] text-amber-400 hover:underline"
|
||||||
|
>
|
||||||
|
{copiedText === 'py' ? 'Copied Python!' : 'Copy Python Code'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="bg-slate-950 border border-slate-800 p-3 rounded-xl text-[11px] font-mono text-amber-200/90 overflow-x-auto">
|
||||||
|
{pythonCode}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Node.js / TypeScript Snippet */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 space-y-3 shadow-xl">
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-800 pb-2">
|
||||||
|
<h4 className="font-bold text-xs text-slate-200 flex items-center space-x-2">
|
||||||
|
<Code2 className="w-4 h-4 text-indigo-400" />
|
||||||
|
<span>Node.js / TypeScript Client Script</span>
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(nodeCode, 'js')}
|
||||||
|
className="text-[11px] text-indigo-400 hover:underline"
|
||||||
|
>
|
||||||
|
{copiedText === 'js' ? 'Copied JS!' : 'Copy Node Code'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="bg-slate-950 border border-slate-800 p-3 rounded-xl text-[11px] font-mono text-indigo-200/90 overflow-x-auto">
|
||||||
|
{nodeCode}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tab 2: Interactive MCP Tool Tester */}
|
||||||
|
{activeTab === 'tester' && (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||||
|
{/* Left: Tool Selection & JSON Input (5 cols) */}
|
||||||
|
<div className="lg:col-span-5 bg-slate-900 border border-slate-800 rounded-2xl p-5 space-y-4 shadow-xl">
|
||||||
|
<h3 className="font-extrabold text-sm text-slate-100 flex items-center space-x-2 border-b border-slate-800 pb-3">
|
||||||
|
<Play className="w-4 h-4 text-emerald-400" />
|
||||||
|
<span>Select & Configure MCP Tool</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3 text-xs">
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-400 mb-1 font-semibold">Exposed MCP Tool</label>
|
||||||
|
<select
|
||||||
|
value={selectedTool}
|
||||||
|
onChange={(e) => {
|
||||||
|
const tool = e.target.value;
|
||||||
|
setSelectedTool(tool);
|
||||||
|
if (tool === 'list_vms') setTesterArgs('{}');
|
||||||
|
else if (tool === 'execute_adb_command')
|
||||||
|
setTesterArgs(JSON.stringify({ vmId: vms[0]?.id || 'vm-cloud-001', command: 'input tap 500 800' }, null, 2));
|
||||||
|
else if (tool === 'assign_proxy')
|
||||||
|
setTesterArgs(JSON.stringify({ vmId: vms[0]?.id || 'vm-cloud-001', host: '104.28.19.42', port: 1080, type: 'SOCKS5' }, null, 2));
|
||||||
|
else if (tool === 'install_apk')
|
||||||
|
setTesterArgs(JSON.stringify({ vmId: vms[0]?.id || 'vm-cloud-001', appName: 'TikTok', packageName: 'com.zhiliaoapp.musically' }, null, 2));
|
||||||
|
else if (tool === 'power_vm')
|
||||||
|
setTesterArgs(JSON.stringify({ vmId: vms[0]?.id || 'vm-cloud-001', action: 'reboot' }, null, 2));
|
||||||
|
else setTesterArgs(JSON.stringify({ vmId: vms[0]?.id || 'vm-cloud-001' }, null, 2));
|
||||||
|
}}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-indigo-300 font-mono font-bold focus:outline-none focus:border-indigo-500"
|
||||||
|
>
|
||||||
|
<option value="list_vms">list_vms — List all active Android VMs</option>
|
||||||
|
<option value="execute_adb_command">execute_adb_command — Run ADB Taps / Shell Commands</option>
|
||||||
|
<option value="assign_proxy">assign_proxy — Assign Proxifly SOCKS5/HTTP Proxy</option>
|
||||||
|
<option value="install_apk">install_apk — Sideload APK package</option>
|
||||||
|
<option value="launch_app">launch_app — Open app package</option>
|
||||||
|
<option value="power_vm">power_vm — Control VM power state</option>
|
||||||
|
<option value="create_vm">create_vm — Provision new VM</option>
|
||||||
|
<option value="get_vm_screenshot">get_vm_screenshot — Get screen state & logs</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-400 mb-1 font-semibold">JSON Arguments Payload</label>
|
||||||
|
<textarea
|
||||||
|
value={testerArgs}
|
||||||
|
onChange={(e) => setTesterArgs(e.target.value)}
|
||||||
|
className="w-full h-44 bg-slate-950 border border-slate-800 rounded-xl p-3 text-xs font-mono text-emerald-300 focus:outline-none focus:border-indigo-500 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleTestMcpTool}
|
||||||
|
disabled={testingTool}
|
||||||
|
className="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-2.5 rounded-xl text-xs shadow-lg shadow-indigo-600/30 flex items-center justify-center space-x-2 transition-all"
|
||||||
|
>
|
||||||
|
<Zap className={`w-4 h-4 text-amber-300 ${testingTool ? 'animate-bounce' : ''}`} />
|
||||||
|
<span>{testingTool ? 'Executing Tool Call...' : 'Execute MCP Tool Call'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Live Payload Inspector (7 cols) */}
|
||||||
|
<div className="lg:col-span-7 bg-slate-900 border border-slate-800 rounded-2xl p-5 space-y-4 shadow-xl flex flex-col">
|
||||||
|
<h3 className="font-extrabold text-sm text-slate-100 flex items-center justify-between border-b border-slate-800 pb-3">
|
||||||
|
<span className="flex items-center space-x-2">
|
||||||
|
<Code2 className="w-4 h-4 text-indigo-400" />
|
||||||
|
<span>Live JSON-RPC 2.0 Response Inspector</span>
|
||||||
|
</span>
|
||||||
|
{testerResponse && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(testerResponse, 'resp')}
|
||||||
|
className="text-[11px] text-indigo-400 hover:underline flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<Copy className="w-3 h-3" />
|
||||||
|
<span>{copiedText === 'resp' ? 'Copied!' : 'Copy Output'}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex-1 bg-slate-950 border border-slate-800 rounded-xl p-4 font-mono text-xs overflow-auto max-h-[450px]">
|
||||||
|
{testerResponse ? (
|
||||||
|
<pre className="text-emerald-300 leading-relaxed">{testerResponse}</pre>
|
||||||
|
) : (
|
||||||
|
<div className="text-slate-600 text-center py-20">
|
||||||
|
<Bot className="w-10 h-10 mx-auto opacity-30 mb-2" />
|
||||||
|
<p>Click "Execute MCP Tool Call" to trigger a live JSON-RPC request to <code className="text-indigo-400">/api/mcp</code></p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tab 3: API Keys Management */}
|
||||||
|
{activeTab === 'keys' && (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||||
|
<div className="lg:col-span-5 bg-slate-900 border border-slate-800 rounded-2xl p-5 space-y-4 shadow-xl">
|
||||||
|
<h3 className="font-extrabold text-sm text-slate-100 flex items-center space-x-2 border-b border-slate-800 pb-3">
|
||||||
|
<Plus className="w-4 h-4 text-emerald-400" />
|
||||||
|
<span>Generate New Bearer API Key</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<form onSubmit={handleCreateApiKey} className="space-y-3 text-xs">
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-400 mb-1 font-semibold">Key Name / Label</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. Cursor Assistant Key / Production Agent"
|
||||||
|
value={newKeyName}
|
||||||
|
onChange={(e) => setNewKeyName(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-slate-200 focus:outline-none focus:border-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!newKeyName.trim() || loading}
|
||||||
|
className={`w-full py-2.5 rounded-xl font-bold text-xs transition-all ${
|
||||||
|
!newKeyName.trim() || loading
|
||||||
|
? 'bg-slate-800 text-slate-500'
|
||||||
|
: 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-md'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Generate Secret API Key
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{createdKeySecret && (
|
||||||
|
<div className="p-3 bg-emerald-500/10 border border-emerald-500/30 rounded-xl space-y-1 text-xs">
|
||||||
|
<p className="font-bold text-emerald-300">Secret Key Created (Copy Now!):</p>
|
||||||
|
<div className="flex items-center justify-between bg-slate-950 p-2 rounded-lg font-mono text-emerald-400">
|
||||||
|
<span className="truncate">{createdKeySecret}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(createdKeySecret, 'createdSec')}
|
||||||
|
className="p-1 text-emerald-300 hover:text-white"
|
||||||
|
>
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-7 bg-slate-900 border border-slate-800 rounded-2xl p-5 space-y-4 shadow-xl">
|
||||||
|
<h3 className="font-extrabold text-sm text-slate-100 flex items-center space-x-2 border-b border-slate-800 pb-3">
|
||||||
|
<ShieldCheck className="w-4 h-4 text-indigo-400" />
|
||||||
|
<span>Active Agent API Keys</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{apiKeys.map((k) => (
|
||||||
|
<div
|
||||||
|
key={k.id}
|
||||||
|
className="p-3.5 bg-slate-950 border border-slate-800 rounded-xl flex items-center justify-between text-xs"
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="font-bold text-slate-100">{k.name}</p>
|
||||||
|
<p className="font-mono text-slate-400 text-[11px]">
|
||||||
|
{k.key.substring(0, 18)}•••••••••••••
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-slate-500">
|
||||||
|
Requests: {k.requestCount} • Created: {new Date(k.createdAt).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleRevokeApiKey(k.id)}
|
||||||
|
className="p-2 text-slate-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-lg transition-colors"
|
||||||
|
title="Revoke key"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tab 4: REST API & OpenAPI Explorer */}
|
||||||
|
{activeTab === 'docs' && (
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 space-y-6 shadow-xl">
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-800 pb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-extrabold text-slate-100">Hypervisor REST API Reference</h3>
|
||||||
|
<p className="text-slate-400 text-xs">Full RESTful endpoints available to your external AI or scripts</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="/api/openapi.json"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="px-3 py-1.5 bg-slate-950 border border-slate-800 text-indigo-400 hover:underline rounded-xl text-xs flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<span>View openapi.json</span>
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 text-xs">
|
||||||
|
{[
|
||||||
|
{ method: 'POST', path: '/api/mcp', desc: 'JSON-RPC 2.0 Model Context Protocol Endpoint for AI Agents' },
|
||||||
|
{ method: 'GET', path: '/api/mcp/sse', desc: 'Server-Sent Events (SSE) Stream for real-time agent subscriptions' },
|
||||||
|
{ method: 'GET', path: '/api/vms', desc: 'List all active Android virtual machines with full hardware metrics' },
|
||||||
|
{ method: 'POST', path: '/api/vms', desc: 'Provision new Android VM instance with custom RAM/vCPU/Profile' },
|
||||||
|
{ method: 'POST', path: '/api/vms/:id/adb', desc: 'Execute ADB shell commands and touch events on target VM' },
|
||||||
|
{ method: 'POST', path: '/api/vms/:id/proxy', desc: 'Assign residential or datacenter Proxifly proxy to VM' },
|
||||||
|
{ method: 'DELETE', path: '/api/vms/:id/proxy', desc: 'Remove network proxy routing from target VM' },
|
||||||
|
{ method: 'POST', path: '/api/vms/assign-proxies-auto', desc: '1-Click auto-assign distinct proxies to all active VMs' },
|
||||||
|
].map((ep, i) => (
|
||||||
|
<div key={i} className="p-3 bg-slate-950 border border-slate-800 rounded-xl flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<span
|
||||||
|
className={`px-2 py-0.5 rounded font-mono font-bold text-[10px] ${
|
||||||
|
ep.method === 'GET' ? 'bg-emerald-500/20 text-emerald-300' : 'bg-indigo-500/20 text-indigo-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{ep.method}
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-slate-200 font-bold">{ep.path}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-slate-400 text-[11px] hidden md:inline">{ep.desc}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
169
src/components/Navbar.tsx
Normal file
169
src/components/Navbar.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Smartphone,
|
||||||
|
Cpu,
|
||||||
|
Terminal,
|
||||||
|
ShieldAlert,
|
||||||
|
CreditCard,
|
||||||
|
Plus,
|
||||||
|
Zap,
|
||||||
|
Activity,
|
||||||
|
CheckCircle2,
|
||||||
|
Sparkles,
|
||||||
|
Server,
|
||||||
|
Layers,
|
||||||
|
Globe,
|
||||||
|
Bot,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { UserSubscription } from '../types';
|
||||||
|
|
||||||
|
export type MainNavTab = 'console' | 'display' | 'proxies' | 'mcp' | 'automation' | 'adb' | 'bootloader' | 'pricing';
|
||||||
|
|
||||||
|
interface NavbarProps {
|
||||||
|
activeTab: MainNavTab;
|
||||||
|
setActiveTab: (tab: MainNavTab) => void;
|
||||||
|
subscription: UserSubscription;
|
||||||
|
onOpenDeployWizard: () => void;
|
||||||
|
onOpenPricingModal: () => void;
|
||||||
|
kvmOnline: boolean;
|
||||||
|
activeVmName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Navbar: React.FC<NavbarProps> = ({
|
||||||
|
activeTab,
|
||||||
|
setActiveTab,
|
||||||
|
subscription,
|
||||||
|
onOpenDeployWizard,
|
||||||
|
onOpenPricingModal,
|
||||||
|
kvmOnline,
|
||||||
|
activeVmName,
|
||||||
|
}) => {
|
||||||
|
const tabs: { id: MainNavTab; label: string; icon: React.ReactNode }[] = [
|
||||||
|
{ id: 'console', label: 'Hypervisor Console', icon: <Server className="w-4 h-4" /> },
|
||||||
|
{ id: 'display', label: 'VM Screen & Touch', icon: <Smartphone className="w-4 h-4" /> },
|
||||||
|
{ id: 'proxies', label: 'Proxies & Proxifly', icon: <Globe className="w-4 h-4 text-indigo-400" /> },
|
||||||
|
{ id: 'mcp', label: 'MCP Server & API', icon: <Bot className="w-4 h-4 text-emerald-400" /> },
|
||||||
|
{ id: 'automation', label: 'Programmatic Automation', icon: <Sparkles className="w-4 h-4 text-amber-400" /> },
|
||||||
|
{ id: 'adb', label: 'ADB Shell & Logcat', icon: <Terminal className="w-4 h-4" /> },
|
||||||
|
{ id: 'bootloader', label: 'Bootloader & Fastboot', icon: <ShieldAlert className="w-4 h-4" /> },
|
||||||
|
{ id: 'pricing', label: 'Rental & Pricing', icon: <CreditCard className="w-4 h-4" /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
const isFreePlan = subscription.plan === 'free';
|
||||||
|
const isLimitReached = subscription.activeVmCount >= subscription.maxVms;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-40 bg-slate-950/95 backdrop-blur-md border-b border-slate-800 text-slate-100">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-center justify-between h-16 gap-4">
|
||||||
|
{/* Logo & Title */}
|
||||||
|
<div className="flex items-center space-x-3 shrink-0">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-emerald-500 via-teal-500 to-cyan-500 p-0.5 flex items-center justify-center shadow-lg shadow-emerald-500/20 shrink-0">
|
||||||
|
<div className="w-full h-full bg-slate-950 rounded-[10px] flex items-center justify-center">
|
||||||
|
<Cpu className="w-5 h-5 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<h1 className="font-bold text-sm sm:text-base tracking-tight text-white flex items-center gap-1.5 whitespace-nowrap">
|
||||||
|
Android Cloud Hypervisor
|
||||||
|
</h1>
|
||||||
|
<span className="hidden xl:inline-block px-2 py-0.5 text-[10px] font-bold tracking-wider uppercase rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
|
||||||
|
KVM / QEMU v8.2
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-slate-400 hidden 2xl:block">
|
||||||
|
Virtual Device Cloud Platform • Multi-OS Bootloader & Fastboot
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Tabs */}
|
||||||
|
<nav className="hidden lg:flex items-center gap-1 bg-slate-900/80 p-1 rounded-xl border border-slate-800 overflow-x-auto no-scrollbar max-w-[45vw] xl:max-w-none">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const isActive = activeTab === tab.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`flex items-center space-x-1.5 px-2.5 py-1.5 rounded-lg text-[11px] font-medium whitespace-nowrap shrink-0 transition-all duration-200 ${
|
||||||
|
isActive
|
||||||
|
? 'bg-emerald-600 text-white shadow-md shadow-emerald-600/20 font-semibold'
|
||||||
|
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.icon}
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Subscription & Actions */}
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
{/* Active Rental Pill */}
|
||||||
|
<button
|
||||||
|
onClick={onOpenPricingModal}
|
||||||
|
className={`flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl border text-[11px] font-medium transition-all shrink-0 ${
|
||||||
|
isFreePlan
|
||||||
|
? 'bg-amber-950/30 border-amber-800/60 text-amber-300 hover:bg-amber-900/40'
|
||||||
|
: 'bg-emerald-950/40 border-emerald-800/60 text-emerald-300 hover:bg-emerald-900/40'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Zap className="w-3.5 h-3.5 text-amber-400 fill-amber-400 shrink-0" />
|
||||||
|
<div className="text-left whitespace-nowrap">
|
||||||
|
<span className="font-bold">
|
||||||
|
{isFreePlan ? 'FREE PLAN' : 'PRO HYPERVISOR'}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] opacity-80 ml-1 font-mono">
|
||||||
|
({subscription.activeVmCount}/{subscription.maxVms})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Upgrade Button */}
|
||||||
|
{isFreePlan && (
|
||||||
|
<button
|
||||||
|
onClick={onOpenPricingModal}
|
||||||
|
className="hidden sm:flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-[11px] font-bold bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 text-white shadow-md shadow-emerald-600/20 transition-all shrink-0 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<Sparkles className="w-3.5 h-3.5 shrink-0" />
|
||||||
|
<span>Rent 5 VMs ($5)</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Deploy New VM Button */}
|
||||||
|
<button
|
||||||
|
onClick={onOpenDeployWizard}
|
||||||
|
className="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-[11px] font-semibold bg-slate-800 hover:bg-slate-700 text-slate-100 border border-slate-700/80 transition-all shadow-sm shrink-0 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span className="hidden sm:inline">Deploy VM</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Navigation Tabs */}
|
||||||
|
<div className="flex lg:hidden overflow-x-auto py-2 space-x-1 no-scrollbar border-t border-slate-800/80">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const isActive = activeTab === tab.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`flex items-center space-x-1.5 px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition-all ${
|
||||||
|
isActive
|
||||||
|
? 'bg-emerald-600 text-white'
|
||||||
|
: 'text-slate-400 hover:text-slate-200 bg-slate-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.icon}
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
240
src/components/PricingModal.tsx
Normal file
240
src/components/PricingModal.tsx
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { UserSubscription } from '../types';
|
||||||
|
import {
|
||||||
|
Check,
|
||||||
|
Zap,
|
||||||
|
Sparkles,
|
||||||
|
ShieldAlert,
|
||||||
|
Smartphone,
|
||||||
|
Cpu,
|
||||||
|
CreditCard,
|
||||||
|
Server,
|
||||||
|
X,
|
||||||
|
Layers,
|
||||||
|
ArrowRight,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface PricingModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
subscription: UserSubscription;
|
||||||
|
onUpgradePlan: (targetPlan: 'free' | 'pro', paymentMethod?: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PricingModal: React.FC<PricingModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
subscription,
|
||||||
|
onUpgradePlan,
|
||||||
|
}) => {
|
||||||
|
const [selectedPlan, setSelectedPlan] = useState<'free' | 'pro'>('pro');
|
||||||
|
const [cardNumber, setCardNumber] = useState('4242 •••• •••• 4242');
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const handleCheckout = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsProcessing(true);
|
||||||
|
await onUpgradePlan(selectedPlan, `Visa ending in ${cardNumber.slice(-4)}`);
|
||||||
|
setIsProcessing(false);
|
||||||
|
setSuccessMessage(
|
||||||
|
selectedPlan === 'pro'
|
||||||
|
? 'Successfully upgraded to Pro Tier ($5/mo)! Your limit expanded to 5 Android VMs.'
|
||||||
|
: 'Switched to Free Plan (1 VM limit).'
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
setSuccessMessage(null);
|
||||||
|
onClose();
|
||||||
|
}, 2500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/80 backdrop-blur-md p-4 overflow-y-auto">
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-3xl max-w-3xl w-full p-6 sm:p-8 space-y-6 relative shadow-2xl my-8">
|
||||||
|
{/* Close Button */}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-5 right-5 text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-800"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Title Header */}
|
||||||
|
<div className="text-center space-y-2 max-w-xl mx-auto">
|
||||||
|
<div className="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-xs font-bold uppercase tracking-wider">
|
||||||
|
<Zap className="w-3.5 h-3.5 fill-current" />
|
||||||
|
<span>Android VM Cloud Hypervisor Pricing</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-extrabold text-white">
|
||||||
|
Simple, Honest Cloud VM Rental
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs sm:text-sm text-slate-400">
|
||||||
|
Rent full virtualized Android instances with built-in bootloader controls, ADB TCP bridging, and root privileges.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Success Alert */}
|
||||||
|
{successMessage && (
|
||||||
|
<div className="p-4 bg-emerald-950/90 border border-emerald-700 text-emerald-300 rounded-2xl text-xs font-bold text-center flex items-center justify-center space-x-2">
|
||||||
|
<Check className="w-5 h-5 text-emerald-400" />
|
||||||
|
<span>{successMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Plan Option Comparison Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
{/* Free Plan Card */}
|
||||||
|
<div
|
||||||
|
onClick={() => setSelectedPlan('free')}
|
||||||
|
className={`p-6 rounded-2xl border cursor-pointer transition-all space-y-4 relative ${
|
||||||
|
selectedPlan === 'free'
|
||||||
|
? 'bg-slate-950 border-amber-500 ring-2 ring-amber-500/20'
|
||||||
|
: 'bg-slate-950/60 border-slate-800 hover:border-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-extrabold text-lg text-white">Free Plan</h3>
|
||||||
|
<p className="text-xs text-slate-400">Perfect for single device testing</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-2xl font-black text-amber-400 font-mono">$0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="py-2 border-y border-slate-800/80 font-mono text-xs text-slate-300 font-bold">
|
||||||
|
1 Active Android VM Instance
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="space-y-2 text-xs text-slate-400">
|
||||||
|
<li className="flex items-center space-x-2">
|
||||||
|
<Check className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>1 Virtualized Android OS (v14 / v13)</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center space-x-2">
|
||||||
|
<Check className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>2 vCPU / 4GB RAM / 32GB Storage</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center space-x-2">
|
||||||
|
<Check className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>Standard WebRTC Screen Viewer</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center space-x-2">
|
||||||
|
<Check className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>ADB Shell Access</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pro Plan Card ($5/mo for 5 VMs) */}
|
||||||
|
<div
|
||||||
|
onClick={() => setSelectedPlan('pro')}
|
||||||
|
className={`p-6 rounded-2xl border cursor-pointer transition-all space-y-4 relative ${
|
||||||
|
selectedPlan === 'pro'
|
||||||
|
? 'bg-slate-950 border-emerald-500 ring-2 ring-emerald-500/30'
|
||||||
|
: 'bg-slate-950/60 border-slate-800 hover:border-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="absolute -top-3 right-4 bg-gradient-to-r from-emerald-500 to-teal-500 text-white font-black text-[10px] uppercase tracking-wider px-3 py-0.5 rounded-full shadow-lg">
|
||||||
|
BEST VALUE • $1 / VM
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-extrabold text-lg text-white flex items-center gap-1.5">
|
||||||
|
<span>Pro Hypervisor</span>
|
||||||
|
<Sparkles className="w-4 h-4 text-emerald-400" />
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-slate-400">For developers, QA, & security research</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-2xl font-black text-emerald-400 font-mono">$5</span>
|
||||||
|
<span className="text-xs text-slate-500">/mo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="py-2 border-y border-slate-800/80 font-mono text-xs text-emerald-400 font-bold">
|
||||||
|
5 Active Android VM Instances
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="space-y-2 text-xs text-slate-300">
|
||||||
|
<li className="flex items-center space-x-2">
|
||||||
|
<Check className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span className="font-semibold text-white">5 Concurrent Android VMs</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center space-x-2">
|
||||||
|
<Check className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>Root Access (Magisk v27 & KernelSU)</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center space-x-2">
|
||||||
|
<Check className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>Bootloader Unlocking & Custom Fastboot</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center space-x-2">
|
||||||
|
<Check className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>High-Performance Hardware Acceleration</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center space-x-2">
|
||||||
|
<Check className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>Snapshot, Backup & Restore Points</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Payment Form */}
|
||||||
|
<form onSubmit={handleCheckout} className="space-y-4 pt-2">
|
||||||
|
{selectedPlan === 'pro' && (
|
||||||
|
<div className="bg-slate-950 p-4 rounded-2xl border border-slate-800 space-y-3">
|
||||||
|
<div className="flex items-center justify-between text-xs font-medium text-slate-300">
|
||||||
|
<span className="flex items-center space-x-1.5">
|
||||||
|
<CreditCard className="w-4 h-4 text-emerald-400" />
|
||||||
|
<span>Payment Details</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-emerald-400 font-mono font-bold">Total: $5.00 USD / month</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={cardNumber}
|
||||||
|
onChange={(e) => setCardNumber(e.target.value)}
|
||||||
|
placeholder="Card Number"
|
||||||
|
required
|
||||||
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3.5 py-2 text-xs font-mono text-slate-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end space-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2.5 rounded-xl text-xs font-bold text-slate-400 hover:bg-slate-800 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isProcessing}
|
||||||
|
className="px-6 py-2.5 rounded-xl text-xs font-bold bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 text-white shadow-lg shadow-emerald-600/25 flex items-center space-x-2 transition-all"
|
||||||
|
>
|
||||||
|
{isProcessing ? (
|
||||||
|
<span>Processing...</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
{selectedPlan === 'pro' ? 'Confirm Pro Rental ($5/mo)' : 'Confirm Free Plan'}
|
||||||
|
</span>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
474
src/components/ProgrammaticAutomationStudio.tsx
Normal file
474
src/components/ProgrammaticAutomationStudio.tsx
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { AndroidVM } from '../types';
|
||||||
|
import {
|
||||||
|
Play,
|
||||||
|
Square,
|
||||||
|
RotateCcw,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Terminal,
|
||||||
|
Cpu,
|
||||||
|
Code,
|
||||||
|
Zap,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertCircle,
|
||||||
|
Copy,
|
||||||
|
Sparkles,
|
||||||
|
Smartphone,
|
||||||
|
Layers,
|
||||||
|
MousePointer,
|
||||||
|
Type,
|
||||||
|
ListPlus,
|
||||||
|
PlayCircle,
|
||||||
|
FileCode,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface ProgrammaticAutomationStudioProps {
|
||||||
|
vm: AndroidVM;
|
||||||
|
onRunAdbCommand: (cmd: string) => Promise<{ output: string; exitCode: number }>;
|
||||||
|
onLaunchApp: (pkg: string) => void;
|
||||||
|
onInstallApk: (appName: string, pkgName: string) => void;
|
||||||
|
inspectorCoords: { x: number; y: number } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationStep {
|
||||||
|
id: string;
|
||||||
|
type: 'start_app' | 'tap' | 'text' | 'keyevent' | 'install' | 'monkey' | 'sleep';
|
||||||
|
target: string;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
delayMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProgrammaticAutomationStudio: React.FC<ProgrammaticAutomationStudioProps> = ({
|
||||||
|
vm,
|
||||||
|
onRunAdbCommand,
|
||||||
|
onLaunchApp,
|
||||||
|
onInstallApk,
|
||||||
|
inspectorCoords,
|
||||||
|
}) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<'macros' | 'adb_terminal' | 'inspector'>('macros');
|
||||||
|
|
||||||
|
// Script Steps
|
||||||
|
const [steps, setSteps] = useState<AutomationStep[]>([
|
||||||
|
{ id: 's1', type: 'start_app', target: 'com.facebook.katana' },
|
||||||
|
{ id: 's2', type: 'sleep', target: '1000', delayMs: 1000 },
|
||||||
|
{ id: 's3', type: 'tap', target: 'Feed Tab', x: 180, y: 120 },
|
||||||
|
{ id: 's4', type: 'text', target: 'Automated Post Test' },
|
||||||
|
{ id: 's5', type: 'keyevent', target: 'ENTER' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Execution states
|
||||||
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
const [currentStepIndex, setCurrentStepIndex] = useState<number | null>(null);
|
||||||
|
const [executionLogs, setExecutionLogs] = useState<string[]>([
|
||||||
|
'Automation Studio initialized.',
|
||||||
|
`Target VM: ${vm.name} (${vm.ipAddress}:${vm.adbPort})`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Terminal Input State
|
||||||
|
const [cliCommand, setCliCommand] = useState('');
|
||||||
|
const [cliHistory, setCliHistory] = useState<{ cmd: string; output: string }[]>([]);
|
||||||
|
|
||||||
|
// Add Step Form
|
||||||
|
const [newStepType, setNewStepType] = useState<AutomationStep['type']>('tap');
|
||||||
|
const [newStepTarget, setNewStepTarget] = useState('');
|
||||||
|
const [newStepX, setNewStepX] = useState('200');
|
||||||
|
const [newStepY, setNewStepY] = useState('400');
|
||||||
|
|
||||||
|
// Pre-built Macro Presets
|
||||||
|
const macroPresets = [
|
||||||
|
{
|
||||||
|
name: 'Facebook Launch & Interaction Macro',
|
||||||
|
description: 'Installs Facebook (if needed), launches app, taps Feed, & injects comment text.',
|
||||||
|
steps: [
|
||||||
|
{ id: 'p1', type: 'install' as const, target: 'com.facebook.katana' },
|
||||||
|
{ id: 'p2', type: 'start_app' as const, target: 'com.facebook.katana' },
|
||||||
|
{ id: 'p3', type: 'sleep' as const, target: '1500', delayMs: 1500 },
|
||||||
|
{ id: 'p4', type: 'tap' as const, target: 'Like Button', x: 80, y: 480 },
|
||||||
|
{ id: 'p5', type: 'text' as const, target: 'Automated comment from ADB runner!' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'UI Stress Test (Monkey Runner)',
|
||||||
|
description: 'Fires 100 automated random touch events across top app activity.',
|
||||||
|
steps: [
|
||||||
|
{ id: 'm1', type: 'monkey' as const, target: vm.currentAppPackage },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'System Property & Network Inspector',
|
||||||
|
description: 'Programmatically dumps getprop, ip config, and active battery status.',
|
||||||
|
steps: [
|
||||||
|
{ id: 'd1', type: 'keyevent' as const, target: 'GETPROP' },
|
||||||
|
{ id: 'd2', type: 'keyevent' as const, target: 'DUMPSYS_BATTERY' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleRunScript = async () => {
|
||||||
|
if (steps.length === 0 || isRunning) return;
|
||||||
|
setIsRunning(true);
|
||||||
|
setExecutionLogs((prev) => [
|
||||||
|
...prev,
|
||||||
|
`=== STARTED AUTOMATION SCRIPT (${steps.length} steps) ===`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
setCurrentStepIndex(i);
|
||||||
|
const step = steps[i];
|
||||||
|
let logMsg = `Step ${i + 1}/${steps.length}: `;
|
||||||
|
|
||||||
|
if (step.type === 'start_app') {
|
||||||
|
logMsg += `[AM START] Launching package ${step.target}`;
|
||||||
|
onLaunchApp(step.target);
|
||||||
|
await onRunAdbCommand(`adb shell am start -n ${step.target}/.MainActivity`);
|
||||||
|
} else if (step.type === 'tap') {
|
||||||
|
const x = step.x || 150;
|
||||||
|
const y = step.y || 300;
|
||||||
|
logMsg += `[INPUT TAP] Simulating touch event at (${x}, ${y})`;
|
||||||
|
await onRunAdbCommand(`adb shell input tap ${x} ${y}`);
|
||||||
|
} else if (step.type === 'text') {
|
||||||
|
logMsg += `[INPUT TEXT] Injecting text: "${step.target}"`;
|
||||||
|
await onRunAdbCommand(`adb shell input text '${step.target}'`);
|
||||||
|
} else if (step.type === 'keyevent') {
|
||||||
|
logMsg += `[KEYEVENT] Triggering hardware key: ${step.target}`;
|
||||||
|
await onRunAdbCommand(`adb shell input keyevent ${step.target}`);
|
||||||
|
} else if (step.type === 'install') {
|
||||||
|
logMsg += `[INSTALL APK] Installing package ${step.target}`;
|
||||||
|
onInstallApk('Custom App', step.target);
|
||||||
|
await onRunAdbCommand(`adb install ${step.target}.apk`);
|
||||||
|
} else if (step.type === 'monkey') {
|
||||||
|
logMsg += `[MONKEY TEST] Running 100 event UI stress test on ${step.target}`;
|
||||||
|
const res = await onRunAdbCommand(`adb shell monkey -p ${step.target} 100`);
|
||||||
|
setExecutionLogs((prev) => [...prev, res.output]);
|
||||||
|
} else if (step.type === 'sleep') {
|
||||||
|
const delay = step.delayMs || 1000;
|
||||||
|
logMsg += `[SLEEP] Delaying execution thread for ${delay}ms`;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
|
||||||
|
setExecutionLogs((prev) => [...prev, logMsg]);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
setExecutionLogs((prev) => [
|
||||||
|
...prev,
|
||||||
|
`SUCCESS: Automation script completed successfully with 0 errors!`,
|
||||||
|
]);
|
||||||
|
setIsRunning(false);
|
||||||
|
setCurrentStepIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddStep = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const step: AutomationStep = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: newStepType,
|
||||||
|
target: newStepTarget || 'default',
|
||||||
|
x: newStepType === 'tap' ? parseInt(newStepX) || 100 : undefined,
|
||||||
|
y: newStepType === 'tap' ? parseInt(newStepY) || 200 : undefined,
|
||||||
|
};
|
||||||
|
setSteps([...steps, step]);
|
||||||
|
setNewStepTarget('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveStep = (id: string) => {
|
||||||
|
setSteps(steps.filter((s) => s.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRunSingleAdb = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!cliCommand.trim()) return;
|
||||||
|
const cmd = cliCommand.trim();
|
||||||
|
setCliCommand('');
|
||||||
|
const res = await onRunAdbCommand(cmd);
|
||||||
|
setCliHistory((prev) => [{ cmd, output: res.output }, ...prev]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 flex flex-col h-full space-y-3 shadow-xl text-slate-100">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="p-2 rounded-xl bg-indigo-500/10 text-indigo-400 border border-indigo-500/20">
|
||||||
|
<Cpu className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-extrabold text-sm text-slate-100 flex items-center space-x-2">
|
||||||
|
<span>Programmatic Automation Studio</span>
|
||||||
|
<span className="px-2 py-0.5 rounded-full bg-emerald-500/20 text-emerald-400 text-[10px] font-mono border border-emerald-500/30">
|
||||||
|
ADB Engine
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-[11px] text-slate-400">
|
||||||
|
Automate app installation, tap gestures, keyboard inputs, & ADB CLI scripts
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Studio Subtabs */}
|
||||||
|
<div className="flex bg-slate-950 p-1 rounded-xl border border-slate-800 text-xs font-semibold">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('macros')}
|
||||||
|
className={`px-3 py-1 rounded-lg transition-all flex items-center space-x-1.5 ${
|
||||||
|
activeTab === 'macros' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<PlayCircle className="w-3.5 h-3.5" />
|
||||||
|
<span>Macro Runner</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('adb_terminal')}
|
||||||
|
className={`px-3 py-1 rounded-lg transition-all flex items-center space-x-1.5 ${
|
||||||
|
activeTab === 'adb_terminal' ? 'bg-indigo-600 text-white shadow-sm' : 'text-slate-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Terminal className="w-3.5 h-3.5" />
|
||||||
|
<span>ADB Terminal</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTab === 'macros' && (
|
||||||
|
<div className="flex-1 grid grid-cols-1 md:grid-cols-12 gap-3 overflow-hidden">
|
||||||
|
{/* Left: Step Builder & Presets (7 cols) */}
|
||||||
|
<div className="md:col-span-7 flex flex-col space-y-3 overflow-y-auto pr-1">
|
||||||
|
{/* Presets Bar */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-1.5">
|
||||||
|
Quick Macro Presets
|
||||||
|
</span>
|
||||||
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
{macroPresets.map((preset, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="p-2.5 bg-slate-950 border border-slate-800 hover:border-slate-700 rounded-xl flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-xs text-indigo-300">{preset.name}</h4>
|
||||||
|
<p className="text-[10px] text-slate-400">{preset.description}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setSteps(preset.steps as AutomationStep[])}
|
||||||
|
className="px-2.5 py-1 bg-indigo-600/20 hover:bg-indigo-600 text-indigo-300 hover:text-white rounded-lg text-[10px] font-bold transition-all shrink-0 ml-2"
|
||||||
|
>
|
||||||
|
Load Macro
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active Script Steps Sequence */}
|
||||||
|
<div className="bg-slate-950 border border-slate-800 rounded-xl p-3 flex-1 flex flex-col space-y-2">
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-800/80 pb-2">
|
||||||
|
<span className="font-bold text-xs text-slate-200 flex items-center space-x-1.5">
|
||||||
|
<ListPlus className="w-4 h-4 text-indigo-400" />
|
||||||
|
<span>Script Steps Sequence ({steps.length})</span>
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setSteps([])}
|
||||||
|
className="text-[10px] text-rose-400 hover:underline"
|
||||||
|
>
|
||||||
|
Clear Steps
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleRunScript}
|
||||||
|
disabled={isRunning || steps.length === 0}
|
||||||
|
className={`px-3 py-1.5 rounded-lg font-bold text-xs flex items-center space-x-1.5 transition-all shadow-md ${
|
||||||
|
isRunning || steps.length === 0
|
||||||
|
? 'bg-slate-800 text-slate-500 cursor-not-allowed'
|
||||||
|
: 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-emerald-950/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Play className="w-3.5 h-3.5 fill-current" />
|
||||||
|
<span>{isRunning ? 'Executing...' : 'Run Script'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Steps List */}
|
||||||
|
<div className="space-y-1.5 max-h-48 overflow-y-auto pr-1">
|
||||||
|
{steps.map((step, idx) => {
|
||||||
|
const isCurrent = currentStepIndex === idx;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={step.id}
|
||||||
|
className={`p-2 rounded-lg border text-xs flex items-center justify-between transition-all ${
|
||||||
|
isCurrent
|
||||||
|
? 'bg-indigo-950 border-indigo-500 text-indigo-200 scale-[1.01]'
|
||||||
|
: 'bg-slate-900 border-slate-800 text-slate-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="w-5 h-5 rounded-full bg-slate-800 text-slate-400 text-[10px] font-mono font-bold flex items-center justify-center">
|
||||||
|
{idx + 1}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<span className="font-bold text-indigo-400 uppercase text-[10px] tracking-wide block">
|
||||||
|
{step.type}
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px]">
|
||||||
|
{step.type === 'tap'
|
||||||
|
? `Tap at X:${step.x}, Y:${step.y}`
|
||||||
|
: step.type === 'start_app'
|
||||||
|
? `Launch Package: ${step.target}`
|
||||||
|
: step.type === 'text'
|
||||||
|
? `Input Text: "${step.target}"`
|
||||||
|
: step.target}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveStep(step.id)}
|
||||||
|
className="text-slate-500 hover:text-rose-400 p-1"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add New Step Form */}
|
||||||
|
<form onSubmit={handleAddStep} className="pt-2 border-t border-slate-800/80 flex items-center space-x-2">
|
||||||
|
<select
|
||||||
|
value={newStepType}
|
||||||
|
onChange={(e) => setNewStepType(e.target.value as any)}
|
||||||
|
className="bg-slate-900 border border-slate-800 rounded-lg px-2 py-1 text-[11px] text-slate-200 font-medium focus:outline-none focus:border-indigo-500"
|
||||||
|
>
|
||||||
|
<option value="tap">TAP (x, y)</option>
|
||||||
|
<option value="start_app">START_APP</option>
|
||||||
|
<option value="text">INPUT_TEXT</option>
|
||||||
|
<option value="keyevent">KEYEVENT</option>
|
||||||
|
<option value="install">INSTALL_APK</option>
|
||||||
|
<option value="sleep">SLEEP</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{newStepType === 'tap' ? (
|
||||||
|
<div className="flex space-x-1 flex-1">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={newStepX}
|
||||||
|
onChange={(e) => setNewStepX(e.target.value)}
|
||||||
|
placeholder="X"
|
||||||
|
className="w-16 bg-slate-900 border border-slate-800 rounded-lg px-2 py-1 text-[11px] text-slate-200"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={newStepY}
|
||||||
|
onChange={(e) => setNewStepY(e.target.value)}
|
||||||
|
placeholder="Y"
|
||||||
|
className="w-16 bg-slate-900 border border-slate-800 rounded-lg px-2 py-1 text-[11px] text-slate-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newStepTarget}
|
||||||
|
onChange={(e) => setNewStepTarget(e.target.value)}
|
||||||
|
placeholder={
|
||||||
|
newStepType === 'start_app'
|
||||||
|
? 'com.facebook.katana'
|
||||||
|
: newStepType === 'text'
|
||||||
|
? 'Enter text...'
|
||||||
|
: 'Target'
|
||||||
|
}
|
||||||
|
className="flex-1 bg-slate-900 border border-slate-800 rounded-lg px-2 py-1 text-[11px] text-slate-200 focus:outline-none"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-3 py-1 bg-indigo-600 hover:bg-indigo-500 text-white font-bold rounded-lg text-[11px]"
|
||||||
|
>
|
||||||
|
Add Step
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Real-time Execution Output Console (5 cols) */}
|
||||||
|
<div className="md:col-span-5 bg-slate-950 border border-slate-800 rounded-xl p-3 flex flex-col font-mono text-[10px] space-y-2">
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-800/80 pb-1.5">
|
||||||
|
<span className="font-bold text-slate-400 flex items-center space-x-1.5">
|
||||||
|
<Code className="w-3.5 h-3.5 text-emerald-400" />
|
||||||
|
<span>Programmatic Execution Logs</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setExecutionLogs([])}
|
||||||
|
className="text-[9px] text-slate-500 hover:text-slate-300"
|
||||||
|
>
|
||||||
|
Clear Console
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 bg-slate-900/60 p-2.5 rounded-lg border border-slate-800/80 overflow-y-auto space-y-1 text-slate-300 leading-relaxed font-mono max-h-80">
|
||||||
|
{executionLogs.map((log, i) => (
|
||||||
|
<div key={i} className="flex space-x-1.5">
|
||||||
|
<span className="text-slate-600 select-none">></span>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
log.includes('SUCCESS')
|
||||||
|
? 'text-emerald-400 font-bold'
|
||||||
|
: log.includes('STARTED')
|
||||||
|
? 'text-indigo-400 font-bold'
|
||||||
|
: 'text-slate-300'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{log}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'adb_terminal' && (
|
||||||
|
<div className="flex-1 bg-slate-950 border border-slate-800 rounded-xl p-3 flex flex-col space-y-3 font-mono text-xs">
|
||||||
|
<div className="flex justify-between items-center text-slate-400 text-[11px] pb-2 border-b border-slate-800">
|
||||||
|
<span>Direct ADB Shell Interface — root@{vm.ipAddress}:{vm.adbPort}</span>
|
||||||
|
<span className="text-emerald-400 font-bold">CONNECTED</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleRunSingleAdb} className="flex items-center space-x-2">
|
||||||
|
<span className="text-indigo-400 font-bold">$ adb shell</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={cliCommand}
|
||||||
|
onChange={(e) => setCliCommand(e.target.value)}
|
||||||
|
placeholder="e.g. input tap 200 400 | am start com.facebook.katana | pm list packages"
|
||||||
|
className="flex-1 bg-slate-900 border border-slate-800 rounded-lg px-3 py-1.5 text-slate-100 font-mono focus:outline-none focus:border-indigo-500 text-xs"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white font-bold rounded-lg text-xs"
|
||||||
|
>
|
||||||
|
Run Command
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="flex-1 bg-slate-900/80 rounded-xl p-3 overflow-y-auto space-y-3 text-slate-300 border border-slate-800 max-h-64">
|
||||||
|
{cliHistory.length === 0 && (
|
||||||
|
<p className="text-slate-500 italic text-[11px]">
|
||||||
|
Type an ADB shell command above and hit Run Command (e.g., `input tap 100 200`, `dumpsys battery`).
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{cliHistory.map((item, idx) => (
|
||||||
|
<div key={idx} className="space-y-1">
|
||||||
|
<p className="text-indigo-300 font-bold text-[11px]">$ adb shell {item.cmd}</p>
|
||||||
|
<pre className="text-slate-400 text-[10px] whitespace-pre-wrap bg-slate-950 p-2 rounded border border-slate-800/80">
|
||||||
|
{item.output}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
619
src/components/ProxyManagerStudio.tsx
Normal file
619
src/components/ProxyManagerStudio.tsx
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { AndroidVM, ProxyConfig, ProxyType } from '../types';
|
||||||
|
import {
|
||||||
|
Globe,
|
||||||
|
Shield,
|
||||||
|
Zap,
|
||||||
|
RefreshCw,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertCircle,
|
||||||
|
Copy,
|
||||||
|
ExternalLink,
|
||||||
|
Layers,
|
||||||
|
Smartphone,
|
||||||
|
Sparkles,
|
||||||
|
Server,
|
||||||
|
Terminal,
|
||||||
|
Search,
|
||||||
|
Filter,
|
||||||
|
Sliders,
|
||||||
|
Radio,
|
||||||
|
Check,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface ProxyManagerStudioProps {
|
||||||
|
vms: AndroidVM[];
|
||||||
|
onAssignProxyToVm: (vmId: string, proxy: ProxyConfig) => Promise<void>;
|
||||||
|
onRemoveProxyFromVm: (vmId: string) => Promise<void>;
|
||||||
|
onAutoAssignProxiesToAllVms: () => Promise<void>;
|
||||||
|
onSelectVm: (vmId: string) => void;
|
||||||
|
selectedVmId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProxyManagerStudio: React.FC<ProxyManagerStudioProps> = ({
|
||||||
|
vms,
|
||||||
|
onAssignProxyToVm,
|
||||||
|
onRemoveProxyFromVm,
|
||||||
|
onAutoAssignProxiesToAllVms,
|
||||||
|
onSelectVm,
|
||||||
|
selectedVmId,
|
||||||
|
}) => {
|
||||||
|
const [proxies, setProxies] = useState<ProxyConfig[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [filterType, setFilterType] = useState<string>('ALL');
|
||||||
|
const [filterCountry, setFilterCountry] = useState<string>('ALL');
|
||||||
|
|
||||||
|
// Custom proxy input form
|
||||||
|
const [targetVmId, setTargetVmId] = useState<string>(selectedVmId || vms[0]?.id || '');
|
||||||
|
const [customHost, setCustomHost] = useState('');
|
||||||
|
const [customPort, setCustomPort] = useState('8080');
|
||||||
|
const [customType, setCustomType] = useState<ProxyType>('SOCKS5');
|
||||||
|
const [customUser, setCustomUser] = useState('');
|
||||||
|
const [customPass, setCustomPass] = useState('');
|
||||||
|
const [customCountry, setCustomCountry] = useState('United States');
|
||||||
|
|
||||||
|
// Bulk raw input
|
||||||
|
const [bulkInput, setBulkInput] = useState('');
|
||||||
|
const [testingHost, setTestingHost] = useState<string | null>(null);
|
||||||
|
const [testResult, setTestResult] = useState<string | null>(null);
|
||||||
|
const [actionSuccess, setActionSuccess] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Fetch Proxifly proxy list on mount
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProxiflyList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedVmId) {
|
||||||
|
setTargetVmId(selectedVmId);
|
||||||
|
}
|
||||||
|
}, [selectedVmId]);
|
||||||
|
|
||||||
|
const fetchProxiflyList = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/proxies/proxifly');
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.proxies) {
|
||||||
|
setProxies(data.proxies);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch Proxifly list', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestProxy = async (host: string, port: number, type: ProxyType) => {
|
||||||
|
setTestingHost(`${host}:${port}`);
|
||||||
|
setTestResult(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/proxies/test', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ host, port, type }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
setTestResult(`ONLINE (${data.latencyMs}ms) - Exit IP: ${data.ipExit}`);
|
||||||
|
} catch (err) {
|
||||||
|
setTestResult('FAILED to connect');
|
||||||
|
} finally {
|
||||||
|
setTestingHost(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssignSelected = async (proxy: ProxyConfig) => {
|
||||||
|
if (!targetVmId) return;
|
||||||
|
setLoading(true);
|
||||||
|
await onAssignProxyToVm(targetVmId, proxy);
|
||||||
|
setActionSuccess(`Assigned ${proxy.type}://${proxy.host}:${proxy.port} to ${vms.find((v) => v.id === targetVmId)?.name}`);
|
||||||
|
setTimeout(() => setActionSuccess(null), 3500);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddCustomAndAssign = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!customHost || !customPort || !targetVmId) return;
|
||||||
|
|
||||||
|
const newProxy: ProxyConfig = {
|
||||||
|
enabled: true,
|
||||||
|
type: customType,
|
||||||
|
host: customHost.trim(),
|
||||||
|
port: parseInt(customPort) || 8080,
|
||||||
|
username: customUser.trim() || undefined,
|
||||||
|
password: customPass.trim() || undefined,
|
||||||
|
country: customCountry || 'Custom Proxy',
|
||||||
|
countryCode: customCountry.substring(0, 2).toUpperCase(),
|
||||||
|
anonymity: 'Elite',
|
||||||
|
latencyMs: 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
await onAssignProxyToVm(targetVmId, newProxy);
|
||||||
|
setProxies((prev) => [newProxy, ...prev]);
|
||||||
|
setActionSuccess(`Custom proxy ${customType}://${customHost}:${customPort} assigned successfully!`);
|
||||||
|
setTimeout(() => setActionSuccess(null), 3500);
|
||||||
|
setCustomHost('');
|
||||||
|
setCustomUser('');
|
||||||
|
setCustomPass('');
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleParseBulk = () => {
|
||||||
|
if (!bulkInput.trim()) return;
|
||||||
|
const lines = bulkInput.split('\n');
|
||||||
|
const parsed: ProxyConfig[] = [];
|
||||||
|
|
||||||
|
lines.forEach((line) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
|
||||||
|
let type: ProxyType = 'SOCKS5';
|
||||||
|
let host = '';
|
||||||
|
let port = 1080;
|
||||||
|
let username: string | undefined;
|
||||||
|
let password: string | undefined;
|
||||||
|
|
||||||
|
let clean = trimmed;
|
||||||
|
if (clean.includes('://')) {
|
||||||
|
const parts = clean.split('://');
|
||||||
|
const proto = parts[0].toUpperCase();
|
||||||
|
if (proto === 'HTTP' || proto === 'HTTPS' || proto === 'SOCKS4' || proto === 'SOCKS5') {
|
||||||
|
type = proto as ProxyType;
|
||||||
|
}
|
||||||
|
clean = parts[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clean.includes('@')) {
|
||||||
|
const [auth, hp] = clean.split('@');
|
||||||
|
const [u, p] = auth.split(':');
|
||||||
|
username = u;
|
||||||
|
password = p;
|
||||||
|
clean = hp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hp = clean.split(':');
|
||||||
|
if (hp.length >= 2) {
|
||||||
|
host = hp[0];
|
||||||
|
port = parseInt(hp[1]) || 8080;
|
||||||
|
if (hp.length >= 4 && !username) {
|
||||||
|
username = hp[2];
|
||||||
|
password = hp[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host) {
|
||||||
|
parsed.push({
|
||||||
|
id: `px-bulk-${Date.now()}-${Math.random()}`,
|
||||||
|
enabled: true,
|
||||||
|
type,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
country: 'Imported List',
|
||||||
|
countryCode: 'IP',
|
||||||
|
anonymity: 'Elite',
|
||||||
|
latencyMs: Math.floor(30 + Math.random() * 50),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parsed.length > 0) {
|
||||||
|
setProxies((prev) => [...parsed, ...prev]);
|
||||||
|
setBulkInput('');
|
||||||
|
setActionSuccess(`Successfully parsed and imported ${parsed.length} proxies!`);
|
||||||
|
setTimeout(() => setActionSuccess(null), 3500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredProxies = proxies.filter((p) => {
|
||||||
|
const matchesType = filterType === 'ALL' || p.type === filterType;
|
||||||
|
const matchesCountry =
|
||||||
|
filterCountry === 'ALL' ||
|
||||||
|
p.country?.toLowerCase().includes(filterCountry.toLowerCase()) ||
|
||||||
|
p.countryCode?.toLowerCase() === filterCountry.toLowerCase();
|
||||||
|
const matchesSearch =
|
||||||
|
p.host.includes(searchQuery) ||
|
||||||
|
p.port.toString().includes(searchQuery) ||
|
||||||
|
p.country?.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
return matchesType && matchesCountry && matchesSearch;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Banner */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 relative overflow-hidden backdrop-blur-sm">
|
||||||
|
<div className="absolute -right-20 -top-20 w-72 h-72 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30 uppercase tracking-wider flex items-center space-x-1">
|
||||||
|
<Globe className="w-3 h-3 text-indigo-400" />
|
||||||
|
<span>Proxifly Developer Hub Integration</span>
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="https://proxifly.dev/tools/proxy-list"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-xs text-indigo-400 hover:underline flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<span>proxifly.dev/tools/proxy-list</span>
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl sm:text-2xl font-extrabold text-white flex items-center space-x-2">
|
||||||
|
<span>Android VM Network Proxy Manager</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-400 text-xs sm:text-sm max-w-2xl">
|
||||||
|
Assign distinct HTTP, HTTPS, SOCKS4, and SOCKS5 residential and datacenter proxies to individual Android Virtual Machines to isolate exit IPs, bypass geo-restrictions, and run multi-account automations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
setLoading(true);
|
||||||
|
await onAutoAssignProxiesToAllVms();
|
||||||
|
setActionSuccess(`Auto-assigned unique Proxifly proxies to all active VMs!`);
|
||||||
|
setTimeout(() => setActionSuccess(null), 3500);
|
||||||
|
setLoading(false);
|
||||||
|
}}
|
||||||
|
disabled={loading || vms.length === 0}
|
||||||
|
className="bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-3 px-5 rounded-xl text-xs shadow-lg shadow-indigo-600/30 flex items-center justify-center space-x-2 transition-all border border-indigo-400/30"
|
||||||
|
>
|
||||||
|
<Sparkles className="w-4 h-4 text-amber-300" />
|
||||||
|
<span>Assign 1-Click Unique Proxies to All VMs ({vms.length})</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{actionSuccess && (
|
||||||
|
<div className="p-3 bg-emerald-500/10 border border-emerald-500/30 text-emerald-300 rounded-xl text-xs flex items-center space-x-2 font-medium animate-fade-in">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>{actionSuccess}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Grid Layout: VM Proxy Assignment Status & Proxifly Live Pool */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||||
|
{/* Left Column: Active VMs & Their Assigned Proxies (5 cols) */}
|
||||||
|
<div className="lg:col-span-5 space-y-4">
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 space-y-4 shadow-xl">
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
|
||||||
|
<h3 className="font-extrabold text-sm text-slate-100 flex items-center space-x-2">
|
||||||
|
<Smartphone className="w-4 h-4 text-indigo-400" />
|
||||||
|
<span>Active VM Proxy Assignments</span>
|
||||||
|
</h3>
|
||||||
|
<span className="text-[11px] font-mono text-slate-400 bg-slate-950 px-2 py-0.5 rounded-lg border border-slate-800">
|
||||||
|
{vms.filter((v) => v.proxy?.enabled).length} / {vms.length} Proxied
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{vms.map((vm) => {
|
||||||
|
const hasProxy = vm.proxy && vm.proxy.enabled;
|
||||||
|
const isSelected = vm.id === targetVmId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={vm.id}
|
||||||
|
onClick={() => setTargetVmId(vm.id)}
|
||||||
|
className={`p-3.5 rounded-xl border transition-all cursor-pointer space-y-2 ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-slate-950 border-indigo-500 shadow-lg shadow-indigo-950/50'
|
||||||
|
: 'bg-slate-950/60 border-slate-800 hover:border-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div
|
||||||
|
className={`w-2 h-2 rounded-full ${
|
||||||
|
hasProxy ? 'bg-emerald-400 shadow-sm shadow-emerald-400' : 'bg-slate-600'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="font-bold text-xs text-slate-100">{vm.name}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-slate-500 font-mono">ADB: {vm.ipAddress}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasProxy ? (
|
||||||
|
<div className="bg-slate-900 border border-slate-800/80 p-2.5 rounded-lg flex items-center justify-between text-xs">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="px-1.5 py-0.5 rounded bg-indigo-500/20 text-indigo-300 font-bold font-mono text-[10px] uppercase">
|
||||||
|
{vm.proxy?.type}
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-slate-200 font-bold">
|
||||||
|
{vm.proxy?.host}:{vm.proxy?.port}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-slate-400 mt-1">
|
||||||
|
{vm.proxy?.country || 'United States'} ({vm.proxy?.countryCode || 'US'}) • Latency: {vm.proxy?.latencyMs || 40}ms
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemoveProxyFromVm(vm.id);
|
||||||
|
}}
|
||||||
|
className="p-1.5 text-slate-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-lg transition-colors"
|
||||||
|
title="Remove proxy from this VM"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-2 bg-slate-900/40 rounded-lg border border-dashed border-slate-800 text-[11px] text-slate-500 flex items-center justify-between">
|
||||||
|
<span>Direct WAN Interface (No proxy assigned)</span>
|
||||||
|
<span className="text-indigo-400 text-[10px] font-bold">Click to assign</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Custom Proxy Form */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 space-y-3">
|
||||||
|
<h4 className="font-bold text-xs text-slate-200 flex items-center space-x-2 border-b border-slate-800 pb-2">
|
||||||
|
<Plus className="w-4 h-4 text-emerald-400" />
|
||||||
|
<span>Assign Custom Single Proxy</span>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<form onSubmit={handleAddCustomAndAssign} className="space-y-2.5 text-xs">
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] text-slate-400 mb-1">Target VM Instance</label>
|
||||||
|
<select
|
||||||
|
value={targetVmId}
|
||||||
|
onChange={(e) => setTargetVmId(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-slate-200 font-medium focus:outline-none focus:border-indigo-500"
|
||||||
|
>
|
||||||
|
{vms.map((v) => (
|
||||||
|
<option key={v.id} value={v.id}>
|
||||||
|
{v.name} ({v.proxy?.enabled ? `Proxied: ${v.proxy.host}` : 'Direct Connection'})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-12 gap-2">
|
||||||
|
<div className="col-span-4">
|
||||||
|
<label className="block text-[11px] text-slate-400 mb-1">Protocol</label>
|
||||||
|
<select
|
||||||
|
value={customType}
|
||||||
|
onChange={(e) => setCustomType(e.target.value as ProxyType)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-2 py-1.5 text-slate-200 font-mono font-bold focus:outline-none"
|
||||||
|
>
|
||||||
|
<option value="SOCKS5">SOCKS5</option>
|
||||||
|
<option value="HTTP">HTTP</option>
|
||||||
|
<option value="HTTPS">HTTPS</option>
|
||||||
|
<option value="SOCKS4">SOCKS4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-5">
|
||||||
|
<label className="block text-[11px] text-slate-400 mb-1">Host / IP</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={customHost}
|
||||||
|
onChange={(e) => setCustomHost(e.target.value)}
|
||||||
|
placeholder="104.28.19.42"
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-slate-200 font-mono focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<label className="block text-[11px] text-slate-400 mb-1">Port</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={customPort}
|
||||||
|
onChange={(e) => setCustomPort(e.target.value)}
|
||||||
|
placeholder="1080"
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-2 py-1.5 text-slate-200 font-mono focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] text-slate-400 mb-1">Username (Optional)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={customUser}
|
||||||
|
onChange={(e) => setCustomUser(e.target.value)}
|
||||||
|
placeholder="user"
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-slate-200 font-mono text-xs focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] text-slate-400 mb-1">Password (Optional)</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={customPass}
|
||||||
|
onChange={(e) => setCustomPass(e.target.value)}
|
||||||
|
placeholder="••••••"
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-slate-200 font-mono text-xs focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!customHost || !targetVmId}
|
||||||
|
className={`w-full py-2 rounded-xl font-bold text-xs transition-all ${
|
||||||
|
!customHost || !targetVmId
|
||||||
|
? 'bg-slate-800 text-slate-500 cursor-not-allowed'
|
||||||
|
: 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-md'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Apply Custom Proxy to Selected VM
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bulk Import Raw Text */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 space-y-2">
|
||||||
|
<h4 className="font-bold text-xs text-slate-200 flex items-center space-x-2">
|
||||||
|
<Terminal className="w-4 h-4 text-indigo-400" />
|
||||||
|
<span>Paste Raw Proxy List (Proxifly / Text)</span>
|
||||||
|
</h4>
|
||||||
|
<textarea
|
||||||
|
value={bulkInput}
|
||||||
|
onChange={(e) => setBulkInput(e.target.value)}
|
||||||
|
placeholder={`Paste ip:port or protocol://ip:port:user:pass lines here:\n104.28.19.42:1080\nsocks5://185.220.101.5:8080:usr:pwd`}
|
||||||
|
className="w-full h-20 bg-slate-950 border border-slate-800 rounded-xl p-2.5 text-[11px] font-mono text-slate-300 focus:outline-none resize-none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleParseBulk}
|
||||||
|
disabled={!bulkInput.trim()}
|
||||||
|
className={`w-full py-1.5 rounded-xl text-xs font-bold transition-colors ${
|
||||||
|
bulkInput.trim() ? 'bg-indigo-600 hover:bg-indigo-500 text-white' : 'bg-slate-800 text-slate-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Parse & Load into Pool
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column: Proxifly Live Proxy Pool List (7 cols) */}
|
||||||
|
<div className="lg:col-span-7 bg-slate-900 border border-slate-800 rounded-2xl p-4 flex flex-col space-y-4 shadow-xl">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-slate-800 pb-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-extrabold text-sm text-slate-100 flex items-center space-x-2">
|
||||||
|
<Globe className="w-4 h-4 text-emerald-400" />
|
||||||
|
<span>Proxifly Verified Proxy Pool</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-[11px] text-slate-400">
|
||||||
|
Click "Assign to VM" on any proxy in the list to route a target VM instance
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={fetchProxiflyList}
|
||||||
|
className="p-2 bg-slate-950 border border-slate-800 hover:border-indigo-500 text-slate-300 hover:text-white rounded-xl text-xs flex items-center space-x-1.5 shrink-0 transition-all"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
<span>Refresh Pool</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Bar */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-12 gap-2 text-xs">
|
||||||
|
<div className="sm:col-span-5 relative">
|
||||||
|
<Search className="w-3.5 h-3.5 absolute left-3 top-2.5 text-slate-500" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Search IP, port, country..."
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-8 pr-3 py-1.5 text-slate-200 text-[11px] focus:outline-none focus:border-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-4 flex items-center space-x-1">
|
||||||
|
<span className="text-[10px] text-slate-500 font-semibold uppercase">Proto:</span>
|
||||||
|
<select
|
||||||
|
value={filterType}
|
||||||
|
onChange={(e) => setFilterType(e.target.value)}
|
||||||
|
className="bg-slate-950 border border-slate-800 rounded-xl px-2 py-1.5 text-slate-200 text-[11px] focus:outline-none flex-1 font-mono font-bold"
|
||||||
|
>
|
||||||
|
<option value="ALL">ALL</option>
|
||||||
|
<option value="SOCKS5">SOCKS5</option>
|
||||||
|
<option value="HTTP">HTTP</option>
|
||||||
|
<option value="HTTPS">HTTPS</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-3 flex items-center space-x-1">
|
||||||
|
<span className="text-[10px] text-slate-500 font-semibold uppercase">Geo:</span>
|
||||||
|
<select
|
||||||
|
value={filterCountry}
|
||||||
|
onChange={(e) => setFilterCountry(e.target.value)}
|
||||||
|
className="bg-slate-950 border border-slate-800 rounded-xl px-2 py-1.5 text-slate-200 text-[11px] focus:outline-none flex-1"
|
||||||
|
>
|
||||||
|
<option value="ALL">ALL</option>
|
||||||
|
<option value="United States">USA</option>
|
||||||
|
<option value="Germany">Germany</option>
|
||||||
|
<option value="Japan">Japan</option>
|
||||||
|
<option value="United Kingdom">UK</option>
|
||||||
|
<option value="France">France</option>
|
||||||
|
<option value="Singapore">Singapore</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Test status banner */}
|
||||||
|
{testResult && (
|
||||||
|
<div className="p-2.5 bg-indigo-950/60 border border-indigo-500/40 text-indigo-200 rounded-xl text-xs flex items-center justify-between font-mono">
|
||||||
|
<span>Ping Test Result: {testResult}</span>
|
||||||
|
<button onClick={() => setTestResult(null)} className="text-[10px] hover:underline text-indigo-400">
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Proxifly Proxies Table / List */}
|
||||||
|
<div className="space-y-2 overflow-y-auto max-h-[500px] pr-1">
|
||||||
|
{filteredProxies.map((proxy, idx) => (
|
||||||
|
<div
|
||||||
|
key={proxy.id || idx}
|
||||||
|
className="p-3 bg-slate-950 border border-slate-800 hover:border-slate-700 rounded-xl flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs transition-all"
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="px-1.5 py-0.5 rounded bg-indigo-500/20 text-indigo-300 font-bold font-mono text-[10px]">
|
||||||
|
{proxy.type}
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-slate-100 font-bold">
|
||||||
|
{proxy.host}:{proxy.port}
|
||||||
|
</span>
|
||||||
|
<span className="px-1.5 py-0.5 rounded bg-slate-800 text-slate-400 text-[9px] font-mono">
|
||||||
|
{proxy.anonymity || 'Elite'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3 text-[11px] text-slate-400">
|
||||||
|
<span>{proxy.country} ({proxy.countryCode})</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span className="text-emerald-400 font-mono font-semibold">
|
||||||
|
{proxy.latencyMs ? `${proxy.latencyMs}ms` : '35ms'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2 shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => handleTestProxy(proxy.host, proxy.port, proxy.type)}
|
||||||
|
disabled={testingHost === `${proxy.host}:${proxy.port}`}
|
||||||
|
className="px-2.5 py-1.5 bg-slate-900 border border-slate-800 hover:border-slate-700 text-slate-300 hover:text-white rounded-lg text-[10px] font-mono transition-colors"
|
||||||
|
>
|
||||||
|
{testingHost === `${proxy.host}:${proxy.port}` ? 'Ping...' : 'Test Ping'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleAssignSelected(proxy)}
|
||||||
|
className="px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white font-bold rounded-lg text-[10px] transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
Assign to Selected VM
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredProxies.length === 0 && (
|
||||||
|
<div className="text-center py-12 text-slate-500 space-y-2">
|
||||||
|
<Globe className="w-8 h-8 mx-auto opacity-30" />
|
||||||
|
<p className="text-xs">No proxies match the current filter or search criteria.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
133
src/data/deviceProfiles.ts
Normal file
133
src/data/deviceProfiles.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { HardwareProfile, AndroidAppPackage } from '../types';
|
||||||
|
|
||||||
|
export class HardwareProfiles {
|
||||||
|
static PIXEL_8_PRO: HardwareProfile = {
|
||||||
|
id: 'pixel-8-pro',
|
||||||
|
name: 'Google Pixel 8 Pro',
|
||||||
|
manufacturer: 'Google',
|
||||||
|
screenResolution: '1440x3120',
|
||||||
|
aspectRatio: '19.5:9',
|
||||||
|
densityDpi: 490,
|
||||||
|
defaultRamGb: 12,
|
||||||
|
defaultVcpu: 8,
|
||||||
|
defaultStorageGb: 128,
|
||||||
|
};
|
||||||
|
|
||||||
|
static GALAXY_S24_ULTRA: HardwareProfile = {
|
||||||
|
id: 'galaxy-s24-ultra',
|
||||||
|
name: 'Samsung Galaxy S24 Ultra',
|
||||||
|
manufacturer: 'Samsung',
|
||||||
|
screenResolution: '1440x3120',
|
||||||
|
aspectRatio: '19.5:9',
|
||||||
|
densityDpi: 505,
|
||||||
|
defaultRamGb: 12,
|
||||||
|
defaultVcpu: 8,
|
||||||
|
defaultStorageGb: 256,
|
||||||
|
};
|
||||||
|
|
||||||
|
static ONEPLUS_12: HardwareProfile = {
|
||||||
|
id: 'oneplus-12',
|
||||||
|
name: 'OnePlus 12',
|
||||||
|
manufacturer: 'OnePlus',
|
||||||
|
screenResolution: '1440x3168',
|
||||||
|
aspectRatio: '19.8:9',
|
||||||
|
densityDpi: 510,
|
||||||
|
defaultRamGb: 16,
|
||||||
|
defaultVcpu: 8,
|
||||||
|
defaultStorageGb: 512,
|
||||||
|
};
|
||||||
|
|
||||||
|
static AOSP_TABLET: HardwareProfile = {
|
||||||
|
id: 'aosp-tablet-10',
|
||||||
|
name: 'AOSP Reference Tablet 10"',
|
||||||
|
manufacturer: 'Generic AOSP',
|
||||||
|
screenResolution: '2560x1600',
|
||||||
|
aspectRatio: '16:10',
|
||||||
|
densityDpi: 320,
|
||||||
|
defaultRamGb: 8,
|
||||||
|
defaultVcpu: 4,
|
||||||
|
defaultStorageGb: 64,
|
||||||
|
};
|
||||||
|
|
||||||
|
static ALL: HardwareProfile[] = [
|
||||||
|
HardwareProfiles.PIXEL_8_PRO,
|
||||||
|
HardwareProfiles.GALAXY_S24_ULTRA,
|
||||||
|
HardwareProfiles.ONEPLUS_12,
|
||||||
|
HardwareProfiles.AOSP_TABLET,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_ANDROID_APPS: AndroidAppPackage[] = [
|
||||||
|
{
|
||||||
|
packageName: 'com.android.launcher3',
|
||||||
|
appName: 'AOSP Launcher',
|
||||||
|
version: '14.0.0',
|
||||||
|
iconName: 'LayoutGrid',
|
||||||
|
isSystem: true,
|
||||||
|
sizeMb: 12.4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageName: 'com.facebook.katana',
|
||||||
|
appName: 'Facebook',
|
||||||
|
version: '450.0.0',
|
||||||
|
iconName: 'Share2',
|
||||||
|
isSystem: false,
|
||||||
|
sizeMb: 85.4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageName: 'com.android.settings',
|
||||||
|
appName: 'Settings',
|
||||||
|
version: '14.0.0',
|
||||||
|
iconName: 'Settings',
|
||||||
|
isSystem: true,
|
||||||
|
sizeMb: 28.1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageName: 'com.android.chrome',
|
||||||
|
appName: 'Chrome Browser',
|
||||||
|
version: '122.0.6261',
|
||||||
|
iconName: 'Globe',
|
||||||
|
isSystem: false,
|
||||||
|
sizeMb: 115.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageName: 'com.topjohnwu.magisk',
|
||||||
|
appName: 'Magisk Manager',
|
||||||
|
version: '27.0',
|
||||||
|
iconName: 'ShieldAlert',
|
||||||
|
isSystem: false,
|
||||||
|
sizeMb: 14.8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageName: 'com.android.vending',
|
||||||
|
appName: 'Google Play Store',
|
||||||
|
version: '39.8.12',
|
||||||
|
iconName: 'ShoppingBag',
|
||||||
|
isSystem: true,
|
||||||
|
sizeMb: 62.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageName: 'com.instagram.android',
|
||||||
|
appName: 'Instagram',
|
||||||
|
version: '315.0.0',
|
||||||
|
iconName: 'Camera',
|
||||||
|
isSystem: false,
|
||||||
|
sizeMb: 72.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageName: 'com.google.android.gm',
|
||||||
|
appName: 'Gmail',
|
||||||
|
version: '2024.03.03',
|
||||||
|
iconName: 'Mail',
|
||||||
|
isSystem: true,
|
||||||
|
sizeMb: 45.2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageName: 'jackpal.androidterm',
|
||||||
|
appName: 'Terminal Emulator',
|
||||||
|
version: '1.0.70',
|
||||||
|
iconName: 'Terminal',
|
||||||
|
isSystem: false,
|
||||||
|
sizeMb: 3.2,
|
||||||
|
},
|
||||||
|
];
|
||||||
9
src/main.tsx
Normal file
9
src/main.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
170
src/types.ts
Normal file
170
src/types.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
export type AndroidVersion =
|
||||||
|
| '15 (VanillaIceCream)'
|
||||||
|
| '14 (UpsideDownCake)'
|
||||||
|
| '13 (Tiramisu)'
|
||||||
|
| '12 (SnowCone)'
|
||||||
|
| '11 (RedVelvetCake)'
|
||||||
|
| '10 (Quince)'
|
||||||
|
| '9.0 (Pie)'
|
||||||
|
| '8.1 (Oreo)';
|
||||||
|
|
||||||
|
export type Architecture = 'x86_64' | 'arm64-v8a';
|
||||||
|
|
||||||
|
export type RootMethod = 'none' | 'magisk' | 'kernelsu';
|
||||||
|
|
||||||
|
export type SELinuxMode = 'enforcing' | 'permissive';
|
||||||
|
|
||||||
|
export type VMStatus = 'booting' | 'running' | 'paused' | 'fastboot' | 'recovery' | 'stopped';
|
||||||
|
|
||||||
|
export type DeviceProfileId =
|
||||||
|
| 'pixel-8-pro'
|
||||||
|
| 'galaxy-s24-ultra'
|
||||||
|
| 'oneplus-12'
|
||||||
|
| 'aosp-tablet-10'
|
||||||
|
| 'android-tv'
|
||||||
|
| 'custom-phone';
|
||||||
|
|
||||||
|
export interface HardwareProfile {
|
||||||
|
id: DeviceProfileId;
|
||||||
|
name: string;
|
||||||
|
manufacturer: string;
|
||||||
|
screenResolution: string; // e.g. "1440x3120"
|
||||||
|
aspectRatio: string;
|
||||||
|
densityDpi: number;
|
||||||
|
defaultRamGb: number;
|
||||||
|
defaultVcpu: number;
|
||||||
|
defaultStorageGb: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BootloaderConfig {
|
||||||
|
isUnlocked: boolean;
|
||||||
|
rootMethod: RootMethod;
|
||||||
|
customRecovery: boolean; // e.g., TWRP
|
||||||
|
selinuxMode: SELinuxMode;
|
||||||
|
kernelCmdline: string;
|
||||||
|
gpuAcceleration: 'swiftshader' | 'virgl' | 'angle' | 'host-passthrough';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AndroidAppPackage {
|
||||||
|
packageName: string;
|
||||||
|
appName: string;
|
||||||
|
version: string;
|
||||||
|
iconName: string;
|
||||||
|
isSystem: boolean;
|
||||||
|
sizeMb: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationCoordinates {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
addressName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VmMetrics {
|
||||||
|
cpuUsagePct: number;
|
||||||
|
ramUsageMb: number;
|
||||||
|
totalRamMb: number;
|
||||||
|
fps: number;
|
||||||
|
networkRxKbps: number;
|
||||||
|
networkTxKbps: number;
|
||||||
|
uptimeSeconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProxyType = 'HTTP' | 'HTTPS' | 'SOCKS4' | 'SOCKS5';
|
||||||
|
|
||||||
|
export interface ProxyConfig {
|
||||||
|
id?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
type: ProxyType;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
country?: string;
|
||||||
|
countryCode?: string;
|
||||||
|
anonymity?: 'Elite' | 'Anonymous' | 'Transparent';
|
||||||
|
latencyMs?: number;
|
||||||
|
lastChecked?: string;
|
||||||
|
externalIp?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AndroidVM {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
androidVersion: AndroidVersion;
|
||||||
|
arch: Architecture;
|
||||||
|
profile: HardwareProfile;
|
||||||
|
ramGb: number;
|
||||||
|
vcpu: number;
|
||||||
|
storageGb: number;
|
||||||
|
bootloader: BootloaderConfig;
|
||||||
|
status: VMStatus;
|
||||||
|
ipAddress: string;
|
||||||
|
adbPort: number;
|
||||||
|
vncPort: number;
|
||||||
|
batteryLevel: number;
|
||||||
|
isCharging: boolean;
|
||||||
|
orientation: 'portrait' | 'landscape';
|
||||||
|
location: LocationCoordinates;
|
||||||
|
metrics: VmMetrics;
|
||||||
|
installedApps: AndroidAppPackage[];
|
||||||
|
currentAppPackage: string; // e.g. 'com.android.launcher3'
|
||||||
|
proxy?: ProxyConfig;
|
||||||
|
createdAt: string;
|
||||||
|
snapshotCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubscriptionPlan = 'free' | 'pro';
|
||||||
|
|
||||||
|
export interface UserSubscription {
|
||||||
|
plan: SubscriptionPlan;
|
||||||
|
maxVms: number; // 1 for free, 5 for pro
|
||||||
|
pricePerMonth: number; // 0 for free, 5 for pro
|
||||||
|
activeVmCount: number;
|
||||||
|
renewsAt: string;
|
||||||
|
paymentMethod?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdbCommandResponse {
|
||||||
|
output: string;
|
||||||
|
exitCode: number;
|
||||||
|
executionTimeMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiKey {
|
||||||
|
id: string;
|
||||||
|
key: string; // sk_proxifly_...
|
||||||
|
name: string;
|
||||||
|
createdAt: string;
|
||||||
|
lastUsedAt?: string;
|
||||||
|
requestCount: number;
|
||||||
|
permissions: 'full_control' | 'read_only';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface McpToolDefinition {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object';
|
||||||
|
properties: Record<string, any>;
|
||||||
|
required?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface McpToolCallRequest {
|
||||||
|
jsonrpc: '2.0';
|
||||||
|
id: string | number;
|
||||||
|
method: string; // e.g. 'tools/list' | 'tools/call' | 'initialize'
|
||||||
|
params?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface McpToolCallResponse {
|
||||||
|
jsonrpc: '2.0';
|
||||||
|
id: string | number;
|
||||||
|
result?: any;
|
||||||
|
error?: {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data?: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user