feat: app 端 ui 设计完成

This commit is contained in:
liqupan
2026-01-28 19:10:19 +08:00
commit a4e7898e94
149 changed files with 11302 additions and 0 deletions

24
wei-ai-demo/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

134
wei-ai-demo/App.tsx Normal file
View File

@@ -0,0 +1,134 @@
import React, { useState, useEffect } from 'react';
import { HashRouter } from 'react-router-dom';
import Layout from './components/Layout';
import Interaction from './pages/Interaction';
import Discovery from './pages/Discovery';
import Library from './pages/Library';
import ManualControl from './pages/ManualControl';
import Profile from './pages/Profile';
import Player from './pages/Player';
import TopUp from './pages/TopUp';
import DeviceManager from './pages/DeviceManager';
import Subscription from './pages/Subscription';
import PrivacySafety from './pages/PrivacySafety';
import HelpFeedback from './pages/HelpFeedback';
import Settings from './pages/Settings';
import { UserTab, Scenario, Character, DeviceStatus } from './types';
const App: React.FC = () => {
const [currentTab, setCurrentTab] = useState<UserTab>(UserTab.Discovery);
const [activeScenario, setActiveScenario] = useState<Scenario | null>(null);
const [activeCharacter, setActiveCharacter] = useState<Character | null>(null);
const [showTopUp, setShowTopUp] = useState(false);
const [showDeviceManager, setShowDeviceManager] = useState(false);
const [showSubscription, setShowSubscription] = useState(false);
const [showPrivacy, setShowPrivacy] = useState(false);
const [showHelp, setShowHelp] = useState(false);
const [showSettings, setShowSettings] = useState(false);
// Global Device State (Mock)
const [deviceStatus, setDeviceStatus] = useState<DeviceStatus>({
connected: true,
battery: 82,
temperature: 36.5,
signalStrength: 90,
currentMode: 'Idle'
});
// Mock battery drain
useEffect(() => {
const interval = setInterval(() => {
setDeviceStatus(prev => ({
...prev,
battery: Math.max(0, prev.battery - (prev.connected ? 0.05 : 0)), // Slow drain
signalStrength: prev.connected ? 85 + Math.random() * 10 : 0
}));
}, 5000);
return () => clearInterval(interval);
}, []);
const handleScenarioPlay = (scenario: Scenario) => {
if (!scenario.isLocked) {
setActiveScenario(scenario);
} else {
alert('Credits required to unlock this premium content.');
}
};
const renderContent = () => {
switch (currentTab) {
case UserTab.Discovery:
return <Discovery onSelectCharacter={setActiveCharacter} />;
case UserTab.Library:
return <Library onPlay={handleScenarioPlay} />;
case UserTab.Control:
return <ManualControl />;
case UserTab.Profile:
return (
<Profile
onTopUp={() => setShowTopUp(true)}
onOpenDeviceManager={() => setShowDeviceManager(true)}
onOpenSubscription={() => setShowSubscription(true)}
onOpenPrivacy={() => setShowPrivacy(true)}
onOpenHelp={() => setShowHelp(true)}
onOpenSettings={() => setShowSettings(true)}
/>
);
default:
return <Discovery onSelectCharacter={setActiveCharacter} />;
}
};
// Define Page Titles based on Tab
const getPageMeta = (tab: UserTab) => {
switch (tab) {
case UserTab.Discovery:
return { title: '专属推荐', subtitle: '基于偏好生成的私密匹配列表' };
case UserTab.Library:
return { title: '剧本馆', subtitle: '沉浸式感官体验库' };
case UserTab.Control:
return { title: '手动实验室', subtitle: '实时触觉反馈控制' };
case UserTab.Profile:
return { title: '个人中心', subtitle: 'ID: 884-291-00X' };
default:
return { title: 'Wei AI', subtitle: '' };
}
};
const meta = getPageMeta(currentTab);
return (
<HashRouter>
{activeScenario ? (
<Player scenario={activeScenario} onClose={() => setActiveScenario(null)} />
) : activeCharacter ? (
<Interaction character={activeCharacter} onBack={() => setActiveCharacter(null)} />
) : showTopUp ? (
<TopUp onBack={() => setShowTopUp(false)} />
) : showDeviceManager ? (
<DeviceManager onBack={() => setShowDeviceManager(false)} />
) : showSubscription ? (
<Subscription onBack={() => setShowSubscription(false)} />
) : showPrivacy ? (
<PrivacySafety onBack={() => setShowPrivacy(false)} />
) : showHelp ? (
<HelpFeedback onBack={() => setShowHelp(false)} />
) : showSettings ? (
<Settings onBack={() => setShowSettings(false)} />
) : (
<Layout
currentTab={currentTab}
onTabChange={setCurrentTab}
title={meta.title}
subtitle={meta.subtitle}
deviceStatus={deviceStatus}
onDeviceClick={() => setShowDeviceManager(true)}
>
{renderContent()}
</Layout>
)}
</HashRouter>
);
};
export default App;

20
wei-ai-demo/README.md Normal file
View File

@@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</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/drive/19FJHfdgVSMTtGhAsRjk32-ezFzN3oYfB
## 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`

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { Compass, PlayCircle, Radio, User } from 'lucide-react';
import { UserTab } from '../types';
interface BottomNavProps {
currentTab: UserTab;
onTabChange: (tab: UserTab) => void;
}
const BottomNav: React.FC<BottomNavProps> = ({ currentTab, onTabChange }) => {
const navItems = [
{ id: UserTab.Discovery, icon: Compass, label: '发现' },
{ id: UserTab.Library, icon: PlayCircle, label: '剧本' },
{ id: UserTab.Control, icon: Radio, label: '操控' },
{ id: UserTab.Profile, icon: User, label: '我的' },
];
return (
<div className="fixed bottom-0 left-0 w-full z-50">
{/* Main Bar Content - High Transparency Glass */}
<div className="bg-black/20 backdrop-blur-2xl border-t border-white/10 pb-safe">
<div className="flex justify-around items-center h-[60px]">
{navItems.map((item) => {
const isActive = currentTab === item.id;
return (
<button
key={item.id}
onClick={() => onTabChange(item.id)}
className="relative flex-1 flex flex-col items-center justify-center h-full group"
>
{/* Active Glow Background */}
{isActive && (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-12 h-12 bg-white/10 rounded-full blur-lg"></div>
)}
{/* Icon */}
<div className={`transition-all duration-300 relative z-10 ${isActive ? 'text-white scale-110 drop-shadow-[0_0_10px_rgba(255,255,255,0.6)]' : 'text-white/50 group-hover:text-white/80'}`}>
<item.icon
size={26}
strokeWidth={isActive ? 2.5 : 1.5}
/>
</div>
{/* Label */}
<span className={`text-[10px] font-medium mt-1 transition-colors duration-300 relative z-10 ${isActive ? 'text-white' : 'text-white/40'}`}>
{item.label}
</span>
</button>
);
})}
</div>
</div>
</div>
);
};
export default BottomNav;

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { Zap, Bluetooth } from 'lucide-react';
import { DeviceStatus } from '../types';
interface HardwareStatusProps {
status: DeviceStatus;
onClick?: () => void;
className?: string;
}
const HardwareStatus: React.FC<HardwareStatusProps> = ({ status, onClick, className = '' }) => {
return (
<button
onClick={onClick}
className={`flex items-center justify-end transition-all duration-500 shrink-0 ${className}`}
>
<div className={`
relative flex items-center h-7 pl-3 pr-3 rounded-full
bg-[#1A1625]/80 backdrop-blur-md border border-white/10 shadow-sm
active:scale-95 transition-all duration-300 group hover:border-[#A855F7]/30
`}>
{/* Connection Status Icon */}
<div className="relative flex items-center justify-center w-3 h-3 mr-2">
{status.connected ? (
<>
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#10B981] opacity-20"></span>
<Bluetooth size={12} className="text-[#10B981] relative z-10" />
</>
) : (
<div className="w-1.5 h-1.5 rounded-full bg-gray-600"></div>
)}
</div>
{/* Divider */}
<div className="w-[1px] h-2.5 bg-white/10 mr-2"></div>
{/* Battery Status */}
{status.connected ? (
<div className="flex items-center gap-1">
<span className={`text-[10px] font-mono font-bold tracking-wide tabular-nums ${status.battery < 20 ? 'text-[#F43F5E]' : 'text-[#E2E8F0]'}`}>
{Math.floor(status.battery)}%
</span>
<Zap
size={9}
className={`${status.battery < 20 ? 'text-[#F43F5E]' : 'text-[#A855F7]'} ${status.battery < 20 ? 'animate-pulse' : ''}`}
fill={status.battery < 20 ? "currentColor" : "currentColor"}
/>
</div>
) : (
<span className="text-[9px] text-gray-500 font-medium tracking-wide"></span>
)}
{/* Subtle Inner Glow */}
<div className="absolute inset-0 rounded-full border border-white/5 pointer-events-none group-hover:border-[#A855F7]/20 transition-colors"></div>
</div>
</button>
);
};
export default HardwareStatus;

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { Hexagon } from 'lucide-react';
import BottomNav from './BottomNav';
import HardwareStatus from './HardwareStatus';
import { UserTab, DeviceStatus } from '../types';
interface LayoutProps {
children: React.ReactNode;
currentTab: UserTab;
onTabChange: (tab: UserTab) => void;
title: string;
subtitle?: string;
deviceStatus: DeviceStatus;
onDeviceClick: () => void;
}
const Layout: React.FC<LayoutProps> = ({
children,
currentTab,
onTabChange,
title,
subtitle,
deviceStatus,
onDeviceClick
}) => {
return (
<div className="h-screen w-screen bg-transparent text-[#F8FAFC] overflow-hidden flex flex-col relative">
{/* 1. Top Template Header (Super Clean) */}
<div className="shrink-0 pt-safe-top z-20">
<div className="px-5 py-4 flex items-end justify-between">
{/* Left: Branding & Title */}
<div className="flex flex-col justify-end">
<div className="flex items-center gap-1.5 mb-1">
<Hexagon size={12} className="text-white fill-white/20" strokeWidth={2.5} />
<span className="text-[10px] font-bold tracking-[0.2em] text-white/80 uppercase shadow-sm">Wei AI</span>
</div>
<h1 className="text-2xl font-bold text-white leading-none tracking-tight drop-shadow-md">{title}</h1>
</div>
{/* Right: Embedded Hardware Status */}
<div className="pb-0.5">
<HardwareStatus status={deviceStatus} onClick={onDeviceClick} />
</div>
</div>
</div>
{/* 2. Middle Content (Scrollable) */}
<main className="flex-1 overflow-y-auto no-scrollbar relative z-10 w-full">
{/* Top fade mask for content scrolling under header */}
<div className="absolute top-0 left-0 w-full h-8 bg-gradient-to-b from-[#2e1065]/20 to-transparent pointer-events-none z-10"></div>
{children}
</main>
{/* 3. Bottom Tab (Fixed) */}
<BottomNav currentTab={currentTab} onTabChange={onTabChange} />
</div>
);
};
export default Layout;

124
wei-ai-demo/constants.ts Normal file
View File

@@ -0,0 +1,124 @@
import { Scenario, Message, Character } from './types';
// Palette - Updated for Lighter/Vibrant theme
export const COLORS = {
bg: '#2e1065',
card: 'rgba(255,255,255,0.1)',
primary: '#C084FC', // Lighter Purple
secondary: '#F472B6', // Lighter Pink
textMain: '#F8FAFC',
textMuted: '#CBD5E1',
};
// 使用 Bing Image Proxy 确保图片稳定加载且符合描述
// 参数: w=宽度, h=高度, c=7(Smart Crop), q=关键词
const getImg = (keyword: string) =>
`https://tse1.mm.bing.net/th?q=${encodeURIComponent(keyword)}&w=600&h=900&c=7&rs=1&p=0&dpr=2&pid=1.7&mkt=en-US&adlt=moderate`;
const getCover = (keyword: string) =>
`https://tse1.mm.bing.net/th?q=${encodeURIComponent(keyword)}&w=400&h=600&c=7&rs=1&p=0&dpr=2&pid=1.7&mkt=en-US&adlt=moderate`;
export const MOCK_CHARACTERS: Character[] = [
{
id: 'c1',
name: 'Eva-09',
tagline: '私人仿生护理专员',
// 关键词: 动漫女孩 白色比基尼 银发 温柔
avatar: getImg('anime girl white bikini silver hair gentle portrait masterpiece'),
description: '专为高压人群设计的仿生人型号,擅长通过精准的触觉反馈缓解神经紧张。',
tags: ['温顺', '医疗', '治愈'],
compatibility: 98,
status: 'online',
},
{
id: 'c2',
name: 'Commander V',
tagline: '深空舰队指挥官',
// 关键词: 动漫女孩 黑色比基尼 军帽 强势
avatar: getImg('anime girl black bikini military cap domineering expression dark hair'),
description: '性格强势,喜欢掌控一切。在连接中,你需要完全服从她的指令。',
tags: ['强势', '指令', '调教'],
compatibility: 85,
status: 'online',
},
{
id: 'c3',
name: 'Yuki (故障版)',
tagline: '觉醒的虚拟偶像',
// 关键词: 动漫女孩 粉色比基尼 赛博朋克 故障风
avatar: getImg('anime girl pink bikini cyberpunk neon colorful hair yandere'),
description: '核心代码出现异常逻辑,表现出极强的占有欲和不可预测的信号波动。',
tags: ['病娇', '不稳定', '高频'],
compatibility: 92,
status: 'busy',
},
{
id: 'c4',
name: 'Secret X',
tagline: '未知信号源',
// 关键词: 动漫女孩 紫色比基尼 神秘 暗黑
avatar: getImg('anime girl purple micro bikini mysterious dark glowing eyes sexy'),
description: '权限不足,请提升会员等级以解码该信号源。',
tags: ['神秘', '极乐'],
compatibility: 0,
status: 'offline',
isLocked: true,
},
];
export const MOCK_SCENARIOS: Scenario[] = [
{
id: '1',
title: '午夜办公室的加班',
category: '职场',
// 关键词: 动漫 办公室 夜晚
cover: getCover('anime girl office lady lingerie night city window'),
duration: '12:30',
intensity: 'Medium',
isLocked: false,
tags: ['沉浸', 'ASMR'],
},
{
id: '2',
title: '私人医生的检查',
category: '角色扮演',
// 关键词: 动漫 护士 诊疗室
cover: getCover('anime nurse girl white bikini hospital room'),
duration: '18:00',
intensity: 'High',
isLocked: true,
tags: ['强互动', '语音'],
},
{
id: '3',
title: '海边度假的偶遇',
category: '邻家',
// 关键词: 动漫 海滩 泳装
cover: getCover('anime girl blue bikini running beach ocean sunny'),
duration: '25:00',
intensity: 'Low',
isLocked: false,
tags: ['纯爱', '剧情'],
},
{
id: '4',
title: '赛博仿生人测试',
category: '科幻',
// 关键词: 动漫 科幻 实验室 机械女
cover: getCover('anime cyborg girl metallic bikini sci-fi lab wires'),
duration: '10:00',
intensity: 'Extreme',
isLocked: true,
tags: ['硬核', '指令'],
},
];
export const INITIAL_MESSAGES: Message[] = [
{
id: '1',
text: '连接已建立,正在校准生物反馈信号...',
sender: 'ai',
type: 'text',
timestamp: Date.now() - 100000,
},
];

67
wei-ai-demo/index.html Normal file
View File

@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="zh-CN" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Wei AI</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #2e1065; /* Fallback */
/* Vibrant Purple-Pink Gradient */
background-image:
radial-gradient(at 0% 0%, rgba(168, 85, 247, 0.4) 0%, transparent 50%),
radial-gradient(at 100% 0%, rgba(236, 72, 153, 0.3) 0%, transparent 50%),
linear-gradient(135deg, #2e1065 0%, #4c1d95 40%, #831843 100%);
background-attachment: fixed;
background-size: cover;
color: #F8FAFC;
-webkit-tap-highlight-color: transparent;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.glass-panel {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
}
.glass-panel-dark {
background: rgba(15, 23, 42, 0.4);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.neon-text {
text-shadow: 0 0 10px rgba(168, 85, 247, 0.6);
}
</style>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@^19.2.3",
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
"react/": "https://esm.sh/react@^19.2.3/",
"recharts": "https://esm.sh/recharts@^3.6.0",
"lucide-react": "https://esm.sh/lucide-react@^0.562.0",
"react-router-dom": "https://esm.sh/react-router-dom@^7.11.0"
}
}
</script>
<link rel="stylesheet" href="/index.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>

15
wei-ai-demo/index.tsx Normal file
View File

@@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,5 @@
{
"name": "Wei AI - 赛博深空",
"description": "Premium AI-driven interactive hardware controller featuring a dark minimalist aesthetic, immersive scenarios, and precise haptic control.",
"requestFramePermissions": []
}

24
wei-ai-demo/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "wei-ai---赛博深空",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.3",
"react-dom": "^19.2.3",
"recharts": "^3.6.0",
"lucide-react": "^0.562.0",
"react-router-dom": "^7.11.0"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}

View File

@@ -0,0 +1,274 @@
import React, { useState, useEffect } from 'react';
import {
ChevronLeft,
Bluetooth,
RefreshCw,
Zap,
Smartphone,
Wifi,
Battery,
Settings,
Power,
Activity,
CheckCircle2
} from 'lucide-react';
interface DeviceManagerProps {
onBack: () => void;
}
type ScanStatus = 'scanning' | 'found' | 'connected';
interface Device {
id: string;
name: string;
signal: number;
isPaired: boolean;
}
const DeviceManager: React.FC<DeviceManagerProps> = ({ onBack }) => {
const [status, setStatus] = useState<ScanStatus>('scanning');
const [scannedDevices, setScannedDevices] = useState<Device[]>([]);
const [connectStep, setConnectStep] = useState(0); // 0: idle, 1: connecting, 2: success
// Simulation: Scan for devices
useEffect(() => {
if (status === 'scanning') {
const timer = setTimeout(() => {
setScannedDevices([
{ id: '1', name: 'Link-X Pro', signal: 92, isPaired: true },
{ id: '2', name: 'Unknown Signal (Weak)', signal: 30, isPaired: false },
]);
setStatus('found');
}, 2500);
return () => clearTimeout(timer);
}
}, [status]);
const handleConnect = (device: Device) => {
setConnectStep(1);
// Simulate connection delay
setTimeout(() => {
setConnectStep(2);
setTimeout(() => {
setStatus('connected');
setConnectStep(0);
}, 1000);
}, 1500);
};
const handleDisconnect = () => {
if(window.confirm('确定要断开与设备的连接吗?')) {
setStatus('scanning');
setScannedDevices([]);
}
};
const testVibration = () => {
if (window.navigator.vibrate) {
window.navigator.vibrate([200, 100, 200]);
}
alert('已发送震动测试指令');
};
// --- RENDER: Connected View ---
if (status === 'connected') {
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in fade-in duration-300">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8"></div>
</div>
<div className="flex-1 overflow-y-auto no-scrollbar p-6">
{/* Device Visual Card */}
<div className="relative w-full aspect-video bg-[#1C1F26] rounded-2xl border border-white/10 overflow-hidden mb-6 flex items-center justify-center group">
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')] opacity-20"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#8B5CF6]/10 via-transparent to-[#0F1014]"></div>
{/* Glowing Center */}
<div className="relative z-10 flex flex-col items-center">
<div className="w-24 h-24 rounded-full bg-[#0F1014] border-2 border-[#8B5CF6] shadow-[0_0_30px_#8B5CF6] flex items-center justify-center mb-4 relative">
<Bluetooth size={40} className="text-white" />
<div className="absolute inset-0 rounded-full border border-[#8B5CF6] animate-ping opacity-20"></div>
</div>
<h2 className="text-xl font-bold text-white tracking-widest">Link-X Pro</h2>
<div className="flex items-center gap-1.5 mt-1">
<div className="w-1.5 h-1.5 rounded-full bg-[#10B981] animate-pulse"></div>
<span className="text-xs text-[#10B981] font-mono"></span>
</div>
</div>
</div>
{/* Status Grid */}
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-[#1C1F26]/60 p-4 rounded-xl border border-white/5 flex flex-col gap-2">
<div className="flex items-center gap-2 text-[#94A3B8]">
<Battery size={16} />
<span className="text-xs"></span>
</div>
<span className="text-2xl font-bold text-white font-mono">85<span className="text-sm text-[#94A3B8]">%</span></span>
<div className="w-full h-1 bg-white/10 rounded-full overflow-hidden">
<div className="h-full w-[85%] bg-[#10B981]"></div>
</div>
</div>
<div className="bg-[#1C1F26]/60 p-4 rounded-xl border border-white/5 flex flex-col gap-2">
<div className="flex items-center gap-2 text-[#94A3B8]">
<Wifi size={16} />
<span className="text-xs"></span>
</div>
<span className="text-2xl font-bold text-white font-mono">-42<span className="text-sm text-[#94A3B8]">dBm</span></span>
<div className="flex gap-1 h-1 mt-auto">
<div className="flex-1 bg-[#8B5CF6] rounded-full"></div>
<div className="flex-1 bg-[#8B5CF6] rounded-full"></div>
<div className="flex-1 bg-[#8B5CF6] rounded-full"></div>
<div className="flex-1 bg-white/10 rounded-full"></div>
</div>
</div>
</div>
{/* Actions */}
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3"></h3>
<div className="space-y-3">
<button onClick={testVibration} className="w-full p-4 bg-[#1C1F26] active:bg-[#2D3039] rounded-xl flex items-center justify-between group transition-all border border-white/5 hover:border-[#8B5CF6]/50">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-[#8B5CF6]/10 text-[#8B5CF6] group-hover:bg-[#8B5CF6] group-hover:text-white transition-colors">
<Activity size={18} />
</div>
<span className="text-sm text-white font-medium"></span>
</div>
<span className="text-xs text-[#64748B]"></span>
</button>
<button className="w-full p-4 bg-[#1C1F26] active:bg-[#2D3039] rounded-xl flex items-center justify-between group transition-all border border-white/5">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-white/5 text-[#94A3B8] group-hover:text-white transition-colors">
<RefreshCw size={18} />
</div>
<div className="flex flex-col items-start">
<span className="text-sm text-white font-medium"></span>
<span className="text-[10px] text-[#64748B]">当前版本: v2.0.4 ()</span>
</div>
</div>
</button>
<button className="w-full p-4 bg-[#1C1F26] active:bg-[#2D3039] rounded-xl flex items-center justify-between group transition-all border border-white/5">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-white/5 text-[#94A3B8] group-hover:text-white transition-colors">
<Settings size={18} />
</div>
<span className="text-sm text-white font-medium"></span>
</div>
</button>
</div>
<button
onClick={handleDisconnect}
className="w-full mt-8 p-4 rounded-xl border border-[#F43F5E]/30 text-[#F43F5E] flex items-center justify-center gap-2 text-sm font-bold active:bg-[#F43F5E]/10 transition-colors"
>
<Power size={16} />
</button>
</div>
</div>
);
}
// --- RENDER: Scanning View ---
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in slide-in-from-right duration-300">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8">
{status === 'scanning' && <RefreshCw size={18} className="text-[#8B5CF6] animate-spin" />}
</div>
</div>
<div className="flex-1 flex flex-col items-center pt-12 px-6">
{/* Radar Animation */}
<div className="relative w-64 h-64 flex items-center justify-center mb-12">
{/* Rings */}
<div className="absolute inset-0 rounded-full border border-white/5"></div>
<div className="absolute inset-12 rounded-full border border-white/5"></div>
<div className="absolute inset-24 rounded-full border border-white/5"></div>
{/* Active Scan Line */}
{status === 'scanning' && (
<div className="absolute inset-0 rounded-full animate-[spin_3s_linear_infinite] bg-gradient-to-tr from-transparent via-transparent to-[#8B5CF6]/20 border-t border-[#8B5CF6]/50" style={{ clipPath: 'polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 50%)' }}></div>
)}
{/* Center Icon */}
<div className="relative z-10 w-20 h-20 bg-[#1C1F26] rounded-full flex items-center justify-center border border-white/10 shadow-[0_0_30px_rgba(0,0,0,0.5)]">
<Bluetooth size={32} className={`${status === 'scanning' ? 'text-[#8B5CF6] animate-pulse' : 'text-white'}`} />
</div>
{/* Status Text */}
<div className="absolute -bottom-10 left-0 w-full text-center">
<p className="text-xs text-[#94A3B8] font-mono tracking-wider">
{status === 'scanning' ? 'SCANNING FOR SIGNALS...' : 'SCAN COMPLETE'}
</p>
</div>
</div>
{/* Device List */}
<div className="w-full space-y-3">
{scannedDevices.map(device => (
<button
key={device.id}
onClick={() => handleConnect(device)}
disabled={connectStep !== 0}
className="w-full bg-[#1C1F26]/80 p-4 rounded-xl flex items-center justify-between border border-white/5 active:scale-[0.98] transition-all group hover:border-[#8B5CF6]/50"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-black/40 flex items-center justify-center text-white">
{connectStep === 1 ? <RefreshCw size={18} className="animate-spin text-[#8B5CF6]" /> : <Smartphone size={18} />}
</div>
<div className="flex flex-col items-start">
<span className="text-sm font-bold text-white group-hover:text-[#8B5CF6] transition-colors">{device.name}</span>
<div className="flex items-center gap-2 mt-0.5">
<div className="flex gap-0.5">
{[1,2,3,4].map(i => (
<div key={i} className={`w-0.5 h-2 rounded-full ${i*25 <= device.signal ? 'bg-[#10B981]' : 'bg-[#334155]'}`}></div>
))}
</div>
<span className="text-[10px] text-[#64748B]">RSSI {device.signal}dBm</span>
</div>
</div>
</div>
{device.isPaired && (
<span className="text-[10px] bg-[#8B5CF6]/20 text-[#8B5CF6] px-2 py-0.5 rounded border border-[#8B5CF6]/30">
</span>
)}
</button>
))}
</div>
{/* Help */}
<div className="mt-auto pb-12 flex items-center gap-2 text-[#64748B] text-xs">
<Zap size={12} />
<span></span>
</div>
</div>
{/* Full Screen Loading Overlay for Connection */}
{connectStep === 2 && (
<div className="absolute inset-0 z-50 bg-[#0F1014]/90 backdrop-blur-md flex flex-col items-center justify-center animate-in fade-in">
<CheckCircle2 size={48} className="text-[#10B981] mb-4 animate-bounce" />
<h3 className="text-lg font-bold text-white"></h3>
</div>
)}
</div>
);
};
export default DeviceManager;

View File

@@ -0,0 +1,123 @@
import React, { useState } from 'react';
import { Lock, Flame } from 'lucide-react';
import { MOCK_CHARACTERS } from '../constants';
import { Character } from '../types';
interface DiscoveryProps {
onSelectCharacter: (char: Character) => void;
}
const Discovery: React.FC<DiscoveryProps> = ({ onSelectCharacter }) => {
const [activeFilter, setActiveFilter] = useState('all');
const filters = [
{ id: 'all', label: '全部' },
{ id: 'gentle', label: '温柔治愈' },
{ id: 'dom', label: '主导强势' },
{ id: 'wild', label: '反差/猎奇' },
{ id: 'voice', label: '语音陪聊' },
{ id: 'scenario', label: '场景扮演' },
{ id: 'exclusive', label: '会员限定' },
];
const filteredCharacters = activeFilter === 'all'
? MOCK_CHARACTERS
: MOCK_CHARACTERS.filter(c => {
const tags = c.tags.join('');
if (activeFilter === 'gentle') return tags.includes('治愈') || tags.includes('温顺') || tags.includes('医疗');
if (activeFilter === 'dom') return tags.includes('强势') || tags.includes('调教') || tags.includes('指令');
if (activeFilter === 'wild') return tags.includes('病娇') || tags.includes('神秘') || tags.includes('不稳定') || tags.includes('极乐');
if (activeFilter === 'exclusive') return c.isLocked;
return false;
});
return (
<div className="pb-24 px-4 h-full">
{/* Filter Bar */}
<div className="sticky top-0 z-20 pt-2 pb-2 -mx-4 mb-4">
<div className="relative">
{/* Scroll Container */}
<div className="flex items-center px-6 gap-3 overflow-x-auto no-scrollbar pr-12">
{filters.map(filter => {
const isActive = activeFilter === filter.id;
return (
<button
key={filter.id}
onClick={() => setActiveFilter(filter.id)}
className={`relative px-4 py-1.5 rounded-full border transition-all duration-300 shrink-0 ${
isActive
? 'bg-white text-[#2e1065] font-bold border-white shadow-[0_0_15px_rgba(255,255,255,0.4)]'
: 'bg-white/5 text-white/70 border-white/10 hover:bg-white/10 backdrop-blur-sm'
}`}
>
<span className="text-sm">{filter.label}</span>
</button>
);
})}
</div>
{/* Right Fade */}
<div className="absolute top-0 right-0 h-full w-12 bg-gradient-to-l from-[#4c1d95]/0 to-transparent pointer-events-none"></div>
</div>
</div>
{/* Grid Layout */}
<div className="grid grid-cols-2 gap-4">
{filteredCharacters.map((char) => (
<div
key={char.id}
onClick={() => !char.isLocked && onSelectCharacter(char)}
className={`relative w-full aspect-[3/4] rounded-3xl overflow-hidden transition-all duration-300 border border-white/20 group ${
char.isLocked ? 'grayscale opacity-70' : 'active:scale-[0.98] shadow-lg hover:shadow-[0_0_25px_rgba(192,132,252,0.3)] hover:border-white/40'
}`}
>
{/* Background Image */}
<img src={char.avatar} alt={char.name} className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110" />
{/* Gradient Overlay - Lighter/Purple based */}
<div className="absolute inset-0 bg-gradient-to-t from-[#2e1065] via-transparent to-transparent opacity-80"></div>
{/* Top Left: Popularity Badge */}
{!char.isLocked && (
<div className="absolute top-3 left-3 flex items-center gap-1 bg-black/40 backdrop-blur-md border border-white/10 pl-2 pr-2.5 py-1 rounded-full shadow-lg z-10">
<Flame size={12} className="text-[#F472B6] fill-[#F472B6]" />
<span className="text-xs font-mono font-bold text-white tracking-wide">
{char.compatibility}%
</span>
</div>
)}
{/* Lock Icon */}
{char.isLocked && (
<div className="absolute top-3 right-3 w-8 h-8 rounded-full bg-black/40 backdrop-blur-md flex items-center justify-center border border-white/20 z-10">
<Lock size={14} className="text-white/80" />
</div>
)}
{/* Content info */}
<div className="absolute bottom-0 left-0 w-full p-4 flex flex-col items-start">
<div className="w-full">
<h2 className="text-lg font-bold text-white leading-tight mb-1 drop-shadow-md">{char.name}</h2>
<div className="flex flex-wrap gap-1.5 opacity-90">
{char.tags.slice(0, 2).map((tag, i) => (
<span key={i} className="text-[10px] text-white bg-white/10 border border-white/10 px-2 py-0.5 rounded-md backdrop-blur-md">
{tag}
</span>
))}
</div>
</div>
</div>
</div>
))}
</div>
{/* Empty State */}
{filteredCharacters.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 text-white/50">
<p className="text-sm"></p>
</div>
)}
</div>
);
};
export default Discovery;

View File

@@ -0,0 +1,150 @@
import React, { useState } from 'react';
import { ChevronLeft, ChevronDown, MessageSquare, BookOpen, Send, HelpCircle, FileText } from 'lucide-react';
interface HelpFeedbackProps {
onBack: () => void;
}
const FAQS = [
{
q: "如何连接 Link-X 设备?",
a: "请确保设备已开机长按电源键3秒蓝灯闪烁。在首页点击底部导航栏的「操控」或在个人中心点击「我的设备」进行搜索配对。"
},
{
q: "订阅会员后权益未生效?",
a: "数据同步可能存在延迟,请尝试下拉刷新个人中心页面,或在设置中点击「恢复购买」。如仍有问题,请提交反馈工单。"
},
{
q: "如何创建自定义剧本?",
a: "Wei AI Pro 用户可在「剧本馆」点击右上角的「+」号进入编辑器支持设置时间轴、震动波形及AI对话逻辑。"
},
{
q: "应用是否会保存我的隐私数据?",
a: "Wei AI 采用端对端加密技术。默认情况下,所有对话记录与震动偏好仅保存在您的本地设备上,除非您手动开启云备份。"
}
];
const HelpFeedback: React.FC<HelpFeedbackProps> = ({ onBack }) => {
const [openFaqIndex, setOpenFaqIndex] = useState<number | null>(0);
const [feedbackText, setFeedbackText] = useState('');
const [isSending, setIsSending] = useState(false);
const toggleFaq = (index: number) => {
setOpenFaqIndex(openFaqIndex === index ? null : index);
};
const handleSendFeedback = () => {
if (!feedbackText.trim()) return;
setIsSending(true);
setTimeout(() => {
setIsSending(false);
setFeedbackText('');
alert('反馈已收到,指挥中心正在分析...');
}, 1500);
};
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in slide-in-from-right duration-300">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8"></div>
</div>
<div className="flex-1 overflow-y-auto no-scrollbar p-5 pb-20">
{/* Quick Actions */}
<div className="grid grid-cols-2 gap-4 mb-8">
<button className="bg-[#1C1F26]/60 p-4 rounded-xl border border-white/5 flex flex-col items-center justify-center gap-2 active:scale-[0.98] transition-transform">
<div className="w-10 h-10 rounded-full bg-[#8B5CF6]/10 flex items-center justify-center text-[#8B5CF6]">
<BookOpen size={20} />
</div>
<span className="text-sm font-bold text-white">使</span>
</button>
<button className="bg-[#1C1F26]/60 p-4 rounded-xl border border-white/5 flex flex-col items-center justify-center gap-2 active:scale-[0.98] transition-transform">
<div className="w-10 h-10 rounded-full bg-[#3B82F6]/10 flex items-center justify-center text-[#3B82F6]">
<MessageSquare size={20} />
</div>
<span className="text-sm font-bold text-white"></span>
</button>
</div>
{/* FAQ Section */}
<div className="mb-8">
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-4 flex items-center gap-2">
<HelpCircle size={12} />
</h3>
<div className="space-y-2">
{FAQS.map((faq, i) => {
const isOpen = openFaqIndex === i;
return (
<div key={i} className="bg-[#1C1F26]/40 border border-white/5 rounded-xl overflow-hidden transition-all duration-300">
<button
onClick={() => toggleFaq(i)}
className="w-full p-4 flex items-center justify-between text-left"
>
<span className={`text-sm font-medium ${isOpen ? 'text-[#8B5CF6]' : 'text-[#E2E8F0]'}`}>
{faq.q}
</span>
<ChevronDown size={16} className={`text-[#64748B] transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`} />
</button>
<div className={`overflow-hidden transition-all duration-300 ${isOpen ? 'max-h-40 opacity-100' : 'max-h-0 opacity-0'}`}>
<div className="px-4 pb-4 pt-0 text-xs text-[#94A3B8] leading-relaxed border-t border-white/5 mt-2">
<div className="pt-3">{faq.a}</div>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Feedback Form */}
<div>
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-4 flex items-center gap-2">
<FileText size={12} />
</h3>
<div className="bg-[#1C1F26]/60 border border-white/5 rounded-xl p-4">
<textarea
value={feedbackText}
onChange={(e) => setFeedbackText(e.target.value)}
placeholder="请描述您遇到的问题或建议..."
className="w-full bg-transparent text-sm text-white placeholder-[#64748B] focus:outline-none min-h-[100px] resize-none"
/>
<div className="flex justify-between items-center mt-3 pt-3 border-t border-white/5">
<span className="text-[10px] text-[#64748B]"> jpg, png </span>
<button
onClick={handleSendFeedback}
disabled={!feedbackText.trim() || isSending}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-bold transition-all ${
feedbackText.trim() && !isSending
? 'bg-[#8B5CF6] text-white shadow-[0_0_15px_rgba(139,92,246,0.3)]'
: 'bg-[#334155] text-[#94A3B8] cursor-not-allowed'
}`}
>
{isSending ? (
<span>...</span>
) : (
<>
<Send size={12} />
</>
)}
</button>
</div>
</div>
</div>
<div className="mt-8 text-center">
<p className="text-[10px] text-[#475569]">Service Code: 884-HELP-V2</p>
</div>
</div>
</div>
);
};
export default HelpFeedback;

View File

@@ -0,0 +1,232 @@
import React, { useState, useRef, useEffect } from 'react';
import { Send, Mic, MicOff, ChevronLeft, MoreVertical, Headphones, PhoneOff, Volume2, VolumeX } from 'lucide-react';
import { Message, Character } from '../types';
interface InteractionProps {
character: Character | null;
onBack: () => void;
}
const Interaction: React.FC<InteractionProps> = ({ character, onBack }) => {
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [isVoiceMode, setIsVoiceMode] = useState(false);
const [isMicMuted, setIsMicMuted] = useState(false);
const [isSpeakerOn, setIsSpeakerOn] = useState(true);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (character) {
setMessages([
{
id: 'init',
text: character.description ? `${character.description}\n\n指挥官${character.name} 已就位。` : '连接建立。',
sender: 'ai',
type: 'text',
timestamp: Date.now(),
}
]);
}
}, [character]);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages]);
const handleSend = () => {
if (!inputValue.trim()) return;
const newMsg: Message = {
id: Date.now().toString(),
text: inputValue,
sender: 'user',
type: 'text',
timestamp: Date.now(),
};
setMessages(prev => [...prev, newMsg]);
setInputValue('');
setTimeout(() => {
const aiMsg: Message = {
id: (Date.now() + 1).toString(),
text: '收到反馈。硬件同步率正在上升...',
sender: 'ai',
type: 'text',
timestamp: Date.now(),
};
setMessages(prev => [...prev, aiMsg]);
}, 1500);
};
if (isVoiceMode && character) {
return (
<div className="fixed inset-0 z-[70] bg-[#2e1065] flex flex-col items-center justify-between py-safe overflow-hidden">
<div className="absolute inset-0 z-0">
<img src={character.avatar} className="w-full h-full object-cover opacity-30 blur-2xl scale-125" />
<div className="absolute inset-0 bg-gradient-to-b from-[#2e1065] via-[#4c1d95]/80 to-[#2e1065]"></div>
</div>
<div className="relative z-10 w-full px-6 pt-6 flex justify-between items-start">
<button onClick={() => setIsVoiceMode(false)} className="p-2 rounded-full bg-white/10 backdrop-blur-sm border border-white/10 text-white/70 active:bg-white/20 transition-colors">
<ChevronLeft size={24} />
</button>
</div>
<div className="relative z-10 flex-1 flex flex-col items-center justify-center w-full">
<div className="text-center mb-12">
<h2 className="text-2xl font-bold text-white tracking-wide drop-shadow-lg mb-2">{character.name}</h2>
<p className="text-white/60 text-xs font-medium tracking-[0.2em] uppercase">
{isMicMuted ? 'Mic Muted' : 'Listening...'}
</p>
</div>
<div className="relative w-48 h-48 flex items-center justify-center">
{!isMicMuted && (
<>
<div className="absolute inset-0 rounded-full border border-white/20 animate-[ping_3s_ease-in-out_infinite] opacity-50"></div>
<div className="absolute inset-0 rounded-full border border-white/10 animate-[ping_3s_ease-in-out_infinite_1s] opacity-30"></div>
</>
)}
<div className="w-40 h-40 rounded-full overflow-hidden border-2 border-white/30 relative shadow-[0_0_50px_rgba(192,132,252,0.5)] z-10">
<img src={character.avatar} className="w-full h-full object-cover" />
</div>
</div>
<div className="mt-12 flex items-center gap-1 h-8">
{[...Array(5)].map((_, i) => (
<div key={i} className={`w-1 bg-white rounded-full transition-all duration-300 ${isMicMuted ? 'h-1 opacity-20' : 'animate-pulse'}`} style={{ height: isMicMuted ? 4 : Math.random() * 24 + 8, animationDelay: `${i * 0.1}s` }}></div>
))}
</div>
</div>
<div className="relative z-10 w-full px-12 pb-12 flex items-center justify-between">
<button
onClick={() => setIsMicMuted(!isMicMuted)}
className={`p-4 rounded-full border transition-all duration-300 ${
isMicMuted
? 'bg-white text-[#2e1065] border-white shadow-lg'
: 'bg-white/10 backdrop-blur-md border-white/10 text-white hover:bg-white/20'
}`}
>
{isMicMuted ? <MicOff size={24} /> : <Mic size={24} />}
</button>
<button
onClick={() => setIsVoiceMode(false)}
className="w-20 h-20 rounded-full bg-red-500 text-white flex items-center justify-center shadow-lg active:scale-95 transition-transform"
>
<PhoneOff size={32} fill="white" />
</button>
<button
onClick={() => setIsSpeakerOn(!isSpeakerOn)}
className={`p-4 rounded-full border transition-all duration-300 ${
isSpeakerOn
? 'bg-white/10 backdrop-blur-md border-white/30 text-white'
: 'bg-white/5 backdrop-blur-md border-white/10 text-white/40'
}`}
>
{isSpeakerOn ? <Volume2 size={24} /> : <VolumeX size={24} />}
</button>
</div>
</div>
);
}
if (!character) return null;
return (
<div className="fixed inset-0 z-[60] bg-[#2e1065] flex flex-col overflow-hidden">
{/* Background */}
<div className="absolute inset-0 z-0 pointer-events-none">
<img
src={character.avatar}
alt="AI Character"
className="w-full h-full object-cover opacity-20 blur-xl scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#2e1065] via-[#2e1065]/90 to-[#2e1065]/50"></div>
</div>
{/* Header */}
<div className="relative z-20 pt-safe px-4 py-3 flex items-center justify-between border-b border-white/10 bg-[#2e1065]/60 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/80 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} strokeWidth={1.5} />
</button>
<div className="flex flex-col items-center">
<h3 className="text-base font-bold text-white tracking-wide drop-shadow-md">{character.name}</h3>
<span className="text-[10px] text-white/60 font-medium tracking-wider uppercase">{character.tagline}</span>
</div>
<button className="p-2 -mr-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<MoreVertical size={20} strokeWidth={1.5} />
</button>
</div>
{/* Chat Area */}
<div className="flex-1 z-10 flex flex-col justify-end pb-2 px-3 overflow-hidden relative">
<div className="absolute top-0 left-0 w-full h-8 bg-gradient-to-b from-[#2e1065]/50 to-transparent z-10 pointer-events-none"></div>
<div
ref={scrollRef}
className="flex-1 overflow-y-auto no-scrollbar space-y-6 pt-4 pb-2"
>
{messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'} group items-end`}>
{msg.sender === 'ai' && (
<div className="w-8 h-8 rounded-full overflow-hidden mr-3 border border-white/20 shrink-0">
<img src={character.avatar} className="w-full h-full object-cover" />
</div>
)}
<div className={`max-w-[80%] p-3 relative ${
msg.sender === 'user'
? 'bg-gradient-to-br from-[#C084FC] to-[#F472B6] text-white rounded-2xl rounded-tr-sm shadow-md'
: 'bg-white/10 backdrop-blur-md border border-white/10 text-white rounded-2xl rounded-tl-sm shadow-sm'
}`}>
{msg.type === 'text' && <p className="text-sm leading-relaxed tracking-wide font-normal">{msg.text}</p>}
</div>
</div>
))}
</div>
{/* Input Area */}
<div className="relative flex items-center gap-2 pt-1 pb-1">
<button
onClick={() => setIsVoiceMode(true)}
className="w-10 h-10 flex items-center justify-center rounded-full transition-all duration-300 border border-white/10 bg-white/10 backdrop-blur-md text-white/70 hover:text-white hover:bg-white/20"
>
<Headphones size={20} strokeWidth={1.5} />
</button>
<div className="flex-1 h-11 bg-white/10 backdrop-blur-md rounded-full flex items-center px-4 border border-white/10 focus-within:border-white/30 focus-within:bg-white/20 transition-all shadow-sm">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="输入指令..."
className="bg-transparent w-full text-sm text-white placeholder-white/40 focus:outline-none"
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
/>
<button className="text-white/50 hover:text-white ml-1 transition-colors">
<Mic size={18} strokeWidth={1.5} />
</button>
</div>
<button
onClick={handleSend}
disabled={!inputValue.trim()}
className={`w-10 h-10 flex items-center justify-center rounded-full transition-all duration-300 ${
inputValue.trim()
? 'bg-white text-[#2e1065] scale-100 opacity-100 shadow-lg'
: 'bg-white/5 text-white/20 scale-95 opacity-50'
}`}
>
<Send size={18} className="-ml-0.5 mt-0.5" strokeWidth={2.5} />
</button>
</div>
</div>
</div>
);
};
export default Interaction;

View File

@@ -0,0 +1,120 @@
import React, { useState } from 'react';
import { Play, Lock, Activity, Zap } from 'lucide-react';
import { MOCK_SCENARIOS } from '../constants';
import { Scenario } from '../types';
interface LibraryProps {
onPlay: (scenario: Scenario) => void;
}
const Library: React.FC<LibraryProps> = ({ onPlay }) => {
const categories = ['全部', '职场', '邻家', '科幻', 'ASMR'];
const [activeCategory, setActiveCategory] = useState('全部');
const filtered = activeCategory === '全部'
? MOCK_SCENARIOS
: MOCK_SCENARIOS.filter(s => s.category === activeCategory || s.tags.includes(activeCategory));
const getIntensityColor = (intensity: string) => {
switch(intensity) {
case 'Low': return 'text-[#34D399]';
case 'Medium': return 'text-[#60A5FA]';
case 'High': return 'text-[#FBBF24]';
case 'Extreme': return 'text-[#F472B6]';
default: return 'text-white/50';
}
};
return (
<div className="pb-24 px-4 min-h-full">
{/* Filter Bar */}
<div className="sticky top-0 z-20 pt-2 pb-2 -mx-4 mb-4">
<div className="relative">
<div className="flex items-center px-6 gap-3 overflow-x-auto no-scrollbar pr-12">
{categories.map(cat => {
const isActive = activeCategory === cat;
return (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`relative px-4 py-1.5 rounded-full border transition-all duration-300 shrink-0 ${
isActive
? 'bg-gradient-to-r from-[#C084FC] to-[#F472B6] text-white font-bold border-transparent shadow-[0_0_15px_rgba(192,132,252,0.4)]'
: 'bg-white/5 text-white/70 border-white/10 hover:bg-white/10 backdrop-blur-md'
}`}
>
<span className="text-sm">{cat}</span>
</button>
);
})}
</div>
<div className="absolute top-0 right-0 h-full w-12 bg-gradient-to-l from-[#4c1d95]/0 to-transparent pointer-events-none"></div>
</div>
</div>
{/* List Layout */}
<div className="flex flex-col gap-3">
{filtered.map(scenario => (
<div
key={scenario.id}
onClick={() => onPlay(scenario)}
className="group relative flex items-center p-3 rounded-2xl bg-[#4c1d95]/20 border border-white/10 overflow-hidden active:scale-[0.98] transition-all duration-300 backdrop-blur-md hover:border-[#C084FC]/40 hover:bg-[#4c1d95]/40 hover:shadow-[0_4px_20px_rgba(0,0,0,0.2)]"
>
{/* Left: Thumbnail */}
<div className="relative w-20 h-20 rounded-xl overflow-hidden shrink-0 shadow-lg border border-white/10">
<img src={scenario.cover} alt={scenario.title} className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" />
{scenario.isLocked && (
<div className="absolute inset-0 bg-[#2e1065]/60 flex items-center justify-center backdrop-blur-[2px]">
<Lock size={16} className="text-white/90" />
</div>
)}
</div>
{/* Center: Info */}
<div className="flex-1 ml-4 flex flex-col justify-center min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-[10px] text-white/90 font-mono uppercase tracking-wider bg-white/10 px-1.5 rounded border border-white/10 backdrop-blur-md">
{scenario.category}
</span>
<div className="flex items-center gap-1">
<Zap size={10} className={getIntensityColor(scenario.intensity)} fill="currentColor" />
<span className={`text-[9px] font-bold ${getIntensityColor(scenario.intensity)}`}>{scenario.intensity}</span>
</div>
</div>
<h3 className="text-sm font-bold text-white mb-1.5 truncate pr-2 group-hover:text-[#F472B6] transition-colors drop-shadow-sm">{scenario.title}</h3>
{/* Tags */}
<div className="flex flex-wrap gap-1.5">
{scenario.tags.slice(0, 3).map((tag, i) => (
<span key={i} className="text-[10px] text-[#E2E8F0] bg-white/5 px-1.5 py-0.5 rounded-sm border border-white/5">#{tag}</span>
))}
</div>
</div>
{/* Right: Action Button */}
<div className="shrink-0 mr-1">
<div className={`w-10 h-10 rounded-full flex items-center justify-center transition-all duration-300 ${
scenario.isLocked
? 'bg-white/5 text-white/30'
: 'bg-[#C084FC]/20 text-[#C084FC] group-hover:bg-gradient-to-r group-hover:from-[#C084FC] group-hover:to-[#F472B6] group-hover:text-white group-hover:shadow-[0_0_15px_rgba(192,132,252,0.5)]'
}`}>
{scenario.isLocked ? <Lock size={16} /> : <Play size={18} fill="currentColor" className="ml-0.5" />}
</div>
</div>
</div>
))}
</div>
{/* Empty State */}
{filtered.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 opacity-50">
<Activity size={32} className="text-white mb-2" />
<p className="text-xs text-white"></p>
</div>
)}
</div>
);
};
export default Library;

View File

@@ -0,0 +1,331 @@
import React, { useState, useRef, useEffect } from 'react';
import {
Activity,
RotateCw,
Bluetooth,
Battery,
ChevronLeft,
Zap,
Sliders,
Waves,
Power
} from 'lucide-react';
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
type ViewMode = 'hub' | 'free' | 'pattern';
type DeviceState = 'disconnected' | 'connecting' | 'connected';
interface Pattern {
id: string;
name: string;
icon: React.ReactNode;
color: string;
}
const PATTERNS: Pattern[] = [
{ id: 'pulse', name: '脉冲跳动', icon: <Activity size={18} />, color: '#C084FC' },
{ id: 'wave', name: '深海潮汐', icon: <Waves size={18} />, color: '#60A5FA' },
{ id: 'climb', name: '登峰造极', icon: <RotateCw size={18} />, color: '#34D399' },
{ id: 'storm', name: '雷雨风暴', icon: <Zap size={18} />, color: '#FBBF24' },
{ id: 'chaos', name: '随机漫步', icon: <Sliders size={18} />, color: '#F472B6' },
{ id: 'sos', name: 'SOS', icon: <Power size={18} />, color: '#F87171' },
];
const DeviceCard: React.FC<{
status: DeviceState;
onConnect: () => void;
onDisconnect: () => void;
}> = ({ status, onConnect, onDisconnect }) => {
return (
<div className="w-full glass-panel p-5 rounded-3xl mb-6 bg-white/10 border border-white/20 relative overflow-hidden shadow-lg">
<div className="absolute -right-6 -top-6 text-white/5">
<Bluetooth size={120} />
</div>
<div className="flex justify-between items-start relative z-10">
<div>
<h3 className="text-white font-bold text-lg flex items-center gap-2">
Link-X Pro
{status === 'connected' && (
<span className="text-[10px] font-bold bg-[#34D399]/20 text-[#34D399] px-2 py-0.5 rounded-full border border-[#34D399]/30 leading-none mt-0.5">
ONLINE
</span>
)}
</h3>
<p className="text-white/60 text-xs font-mono mt-1">
{status === 'disconnected' ? '设备未连接' : status === 'connecting' ? '正在搜索信号...' : 'ID: 884-X9-01'}
</p>
</div>
<button
onClick={status === 'connected' ? onDisconnect : onConnect}
className={`px-5 py-2 rounded-full text-xs font-bold transition-all ${
status === 'connected'
? 'bg-white/10 text-white/70 border border-white/10'
: status === 'connecting'
? 'bg-white/20 text-white animate-pulse'
: 'bg-white text-[#2e1065] shadow-lg hover:shadow-xl'
}`}
>
{status === 'connected' ? '断开' : status === 'connecting' ? '连接中' : '连接设备'}
</button>
</div>
{status === 'connected' && (
<div className="mt-6 flex items-center gap-6 relative z-10">
<div className="flex items-center gap-2">
<Battery size={16} className="text-[#34D399]" />
<span className="text-sm font-mono text-white">85%</span>
</div>
</div>
)}
</div>
);
};
const FreeControlView: React.FC<{ onBack: () => void }> = ({ onBack }) => {
const [intensity, setIntensity] = useState(0);
const [isClimax, setIsClimax] = useState(false);
const controlRef = useRef<HTMLDivElement>(null);
const handleInteraction = (e: React.MouseEvent | React.TouchEvent) => {
if (!controlRef.current) return;
const rect = controlRef.current.getBoundingClientRect();
const clientY = 'touches' in e ? e.touches[0].clientY : (e as React.MouseEvent).clientY;
const relativeY = Math.max(0, Math.min(1, (rect.bottom - clientY) / rect.height));
setIntensity(Math.round(relativeY * 100));
};
const handleClimax = () => {
setIsClimax(true);
setIntensity(100);
if (window.navigator.vibrate) window.navigator.vibrate(500);
setTimeout(() => {
setIsClimax(false);
setIntensity(20);
}, 3000);
};
return (
<div className="h-full flex flex-col">
<div className="flex items-center mb-6">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white">
<ChevronLeft size={24} />
</button>
<h2 className="text-lg font-bold text-white ml-2"></h2>
</div>
<div className="flex-1 flex flex-col items-center justify-center">
<div
ref={controlRef}
className="relative w-48 h-[60vh] rounded-[32px] border border-white/20 bg-white/5 shadow-2xl overflow-hidden touch-none backdrop-blur-xl"
onTouchMove={handleInteraction}
onMouseDown={(e) => e.buttons === 1 && handleInteraction(e)}
onMouseMove={(e) => e.buttons === 1 && handleInteraction(e)}
>
<div
className={`absolute bottom-0 w-full transition-all duration-75 ease-linear backdrop-blur-md flex items-start justify-center pt-2 ${isClimax ? 'bg-red-500/50' : 'bg-[#C084FC]/50'}`}
style={{ height: `${intensity}%` }}
>
<div className={`w-full h-[3px] ${isClimax ? 'bg-red-500 shadow-[0_0_20px_white]' : 'bg-white shadow-[0_0_15px_white]'}`}></div>
</div>
<div className="absolute inset-0 flex flex-col justify-between p-6 pointer-events-none">
<span className="text-xs text-white/40 font-mono mx-auto">MAX</span>
<div className="flex-1 flex items-center justify-center">
<span className={`text-5xl font-bold font-mono tracking-tighter transition-colors drop-shadow-lg ${isClimax ? 'text-red-200' : 'text-white'}`}>
{intensity}
</span>
</div>
<span className="text-xs text-white/40 font-mono mx-auto">OFF</span>
</div>
</div>
<p className="mt-6 text-white/60 text-xs font-mono tracking-wider"></p>
</div>
<button
onClick={handleClimax}
disabled={isClimax}
className={`w-full py-4 mt-6 rounded-2xl font-bold tracking-widest text-white transition-all active:scale-95 shadow-lg ${
isClimax
? 'bg-red-500 animate-pulse cursor-not-allowed'
: 'bg-white/10 border border-red-400/50 text-red-300 hover:bg-red-500/20'
}`}
>
{isClimax ? 'MAX OUTPUT...' : '一键爆发'}
</button>
</div>
);
};
const PatternControlView: React.FC<{ onBack: () => void }> = ({ onBack }) => {
const [activePattern, setActivePattern] = useState<string | null>(null);
const [globalIntensity, setGlobalIntensity] = useState(50);
const generateWaveData = (patternId: string | null) => {
if (!patternId) return Array(20).fill({ v: 10 });
return Array.from({ length: 20 }, (_, i) => ({
v: patternId === 'pulse' ? (i % 5 === 0 ? 90 : 20) :
patternId === 'wave' ? 40 + Math.sin(i) * 30 :
Math.random() * 80 + 10
}));
};
const [waveData, setWaveData] = useState(generateWaveData(null));
useEffect(() => {
if (activePattern) {
const interval = setInterval(() => {
setWaveData(prev => {
const next = [...prev.slice(1), { v: Math.random() * (globalIntensity) + 20 }];
return next;
});
}, 100);
return () => clearInterval(interval);
} else {
setWaveData(Array(20).fill({ v: 10 }));
}
}, [activePattern, globalIntensity]);
return (
<div className="h-full flex flex-col">
<div className="flex items-center mb-6">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white">
<ChevronLeft size={24} />
</button>
<h2 className="text-lg font-bold text-white ml-2"></h2>
</div>
<div className="h-40 w-full glass-panel rounded-3xl mb-8 p-4 flex items-center justify-center relative overflow-hidden bg-white/5 border border-white/10">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={waveData}>
<defs>
<linearGradient id="waveGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#C084FC" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#C084FC" stopOpacity={0}/>
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="v"
stroke="#E9D5FF"
strokeWidth={3}
fill="url(#waveGradient)"
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="mb-8">
<div className="flex justify-between text-xs text-white/60 mb-2 uppercase tracking-wider">
<span></span>
<span>{globalIntensity}%</span>
</div>
<input
type="range"
min="0"
max="100"
value={globalIntensity}
onChange={(e) => setGlobalIntensity(parseInt(e.target.value))}
className="w-full h-2 bg-white/20 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:shadow-lg"
/>
</div>
<div className="grid grid-cols-2 gap-3 overflow-y-auto no-scrollbar pb-6">
{PATTERNS.map(p => {
const isActive = activePattern === p.id;
return (
<button
key={p.id}
onClick={() => setActivePattern(isActive ? null : p.id)}
className={`p-4 rounded-2xl border transition-all duration-300 flex flex-col items-center gap-2 ${
isActive
? 'bg-white text-[#2e1065] border-white shadow-lg scale-[1.02]'
: 'bg-white/5 border-white/10 text-white/60 hover:bg-white/10'
}`}
>
<div className={`p-2 rounded-full ${isActive ? 'bg-[#2e1065]/10 text-[#2e1065]' : 'bg-white/5'}`}>
{p.icon}
</div>
<span className="text-sm font-bold">
{p.name}
</span>
</button>
)
})}
</div>
</div>
);
};
const ManualControl: React.FC = () => {
const [view, setView] = useState<ViewMode>('hub');
const [deviceStatus, setDeviceStatus] = useState<DeviceState>('disconnected');
const connectDevice = () => {
setDeviceStatus('connecting');
setTimeout(() => setDeviceStatus('connected'), 1500);
};
const disconnectDevice = () => {
setDeviceStatus('disconnected');
};
if (view === 'free') return <div className="pt-4 px-6 h-full"><FreeControlView onBack={() => setView('hub')} /></div>;
if (view === 'pattern') return <div className="pt-4 px-6 h-full"><PatternControlView onBack={() => setView('hub')} /></div>;
return (
<div className="pt-4 px-6 h-full flex flex-col">
<div className="mb-2">
<DeviceCard
status={deviceStatus}
onConnect={connectDevice}
onDisconnect={disconnectDevice}
/>
</div>
<h2 className="text-xs font-bold text-white/40 uppercase tracking-widest mb-3"></h2>
<div className="grid grid-cols-1 gap-4">
<button
onClick={() => setView('free')}
disabled={deviceStatus !== 'connected'}
className={`group relative overflow-hidden h-36 rounded-3xl flex items-center px-8 transition-all ${
deviceStatus === 'connected'
? 'bg-white/10 border border-white/20 active:scale-[0.98] backdrop-blur-md'
: 'bg-white/5 border border-white/5 opacity-50 cursor-not-allowed'
}`}
>
<div className="w-14 h-14 rounded-full bg-[#C084FC]/20 flex items-center justify-center text-[#C084FC] group-hover:scale-110 transition-transform">
<Sliders size={28} />
</div>
<div className="ml-5 text-left">
<h3 className="text-xl font-bold text-white group-hover:text-[#C084FC] transition-colors"></h3>
<p className="text-xs text-white/50 mt-1"> </p>
</div>
</button>
<button
onClick={() => setView('pattern')}
disabled={deviceStatus !== 'connected'}
className={`group relative overflow-hidden h-36 rounded-3xl flex items-center px-8 transition-all ${
deviceStatus === 'connected'
? 'bg-white/10 border border-white/20 active:scale-[0.98] backdrop-blur-md'
: 'bg-white/5 border border-white/5 opacity-50 cursor-not-allowed'
}`}
>
<div className="w-14 h-14 rounded-full bg-[#60A5FA]/20 flex items-center justify-center text-[#60A5FA] group-hover:scale-110 transition-transform">
<Waves size={28} />
</div>
<div className="ml-5 text-left">
<h3 className="text-xl font-bold text-white group-hover:text-[#60A5FA] transition-colors"></h3>
<p className="text-xs text-white/50 mt-1">9</p>
</div>
</button>
</div>
</div>
);
};
export default ManualControl;

View File

@@ -0,0 +1,285 @@
import React, { useEffect, useState, useMemo, useRef } from 'react';
import { X, Pause, Play, AlertTriangle } from 'lucide-react';
import { AreaChart, Area, ResponsiveContainer, YAxis, XAxis, ReferenceDot } from 'recharts';
import { Scenario } from '../types';
interface PlayerProps {
scenario: Scenario | null;
onClose: () => void;
}
// Mock script synced with progress (0-100)
const DIALOGUE_SCRIPT = [
{ time: 0, text: "正在建立神经连接..." },
{ time: 5, text: "(检测到心率轻微上升)" },
{ time: 12, text: "“放松,把控制权交给我。”" },
{ time: 20, text: "“很好,保持呼吸频率...”" },
{ time: 28, text: "正在启动触觉反馈模块" },
{ time: 35, text: "“感觉到那个节奏了吗?”" },
{ time: 45, text: "强度正在逐渐增加..." },
{ time: 55, text: "“不要抵抗,顺从它。”" },
{ time: 65, text: "“我会稍微加快一点速度。”" },
{ time: 75, text: "(设备输出功率提升至 80%)" },
{ time: 85, text: "“就是现在...”" },
{ time: 92, text: "“做得很好,指挥官。”" },
{ time: 100, text: "连接结束。" },
];
const Player: React.FC<PlayerProps> = ({ scenario, onClose }) => {
const [isPlaying, setIsPlaying] = useState(true);
const [progress, setProgress] = useState(0);
const [isDragging, setIsDragging] = useState(false); // Track dragging state
const [showAlert, setShowAlert] = useState(false);
// Generate 101 data points (0 to 100) to map perfectly to progress
const data = useMemo(() => {
return Array.from({ length: 101 }, (_, i) => {
// Create a realistic wave: Warmup -> Build up -> Climax -> Cooldown
let hz = 10;
if (i < 20) hz = 10 + (i * 1.5) + (Math.random() * 5); // Warmup
else if (i < 50) hz = 40 + Math.sin(i * 0.5) * 10 + (Math.random() * 10); // Plateau/Tease
else if (i < 85) hz = 70 + (i - 50) * 0.8 + (Math.random() * 15); // Build up to climax
else hz = 90 - ((i - 85) * 5) + (Math.random() * 5); // Cooldown
return {
time: i,
hz: Math.max(0, Math.min(100, hz))
};
});
}, []);
useEffect(() => {
let interval: any;
if (isPlaying && !isDragging) {
interval = setInterval(() => {
setProgress(p => {
if (p >= 100) {
setIsPlaying(false);
return 100;
}
return p + 0.1;
});
}, 50);
}
return () => {
if (interval) clearInterval(interval);
};
}, [isPlaying, isDragging]);
const handleEmergencyStop = () => {
setIsPlaying(false);
setShowAlert(true);
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate([100, 50, 100]);
}
setTimeout(() => {
onClose();
}, 1500);
};
// Handle Seek / Drag
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVal = parseFloat(e.target.value);
setProgress(newVal);
};
const handleDragStart = () => {
setIsDragging(true);
};
const handleDragEnd = () => {
setIsDragging(false);
};
// Calculate active line based on progress
const activeLineIndex = DIALOGUE_SCRIPT.findIndex((line, index) => {
const nextLine = DIALOGUE_SCRIPT[index + 1];
return progress >= line.time && (!nextLine || progress < nextLine.time);
});
// Get current HZ for the dot
const currentHz = data[Math.min(100, Math.floor(progress))]?.hz || 0;
if (!scenario) return null;
return (
<div className="fixed inset-0 z-[100] bg-[#2e1065] flex flex-col" onDoubleClick={handleEmergencyStop}>
{/* Background Visual */}
<div className="absolute inset-0 opacity-40">
<img src={scenario.cover} className="w-full h-full object-cover blur-lg scale-110" alt="bg" />
<div className="absolute inset-0 bg-gradient-to-t from-[#2e1065] via-[#2e1065]/80 to-[#4c1d95]/60"></div>
</div>
{/* Emergency Overlay */}
{showAlert && (
<div className="absolute inset-0 z-[110] bg-[#2e1065]/95 flex flex-col items-center justify-center animate-pulse">
<AlertTriangle size={64} className="text-[#F43F5E] mb-4" />
<h2 className="text-2xl font-bold text-[#F43F5E] tracking-widest">EMERGENCY STOP</h2>
<p className="text-[#E2E8F0] mt-2"></p>
</div>
)}
{/* Header - Seamless Gradient */}
<div className="relative z-10 flex justify-between items-center p-6 bg-gradient-to-b from-[#2e1065] via-[#2e1065]/60 to-transparent">
<div className="flex flex-col">
<h2 className="text-lg font-bold text-white tracking-wide drop-shadow-md">{scenario.title}</h2>
<span className="text-[10px] text-[#C084FC] uppercase tracking-wider font-medium">{scenario.category} SCENARIO</span>
</div>
<button onClick={onClose} className="p-2 rounded-full bg-white/10 backdrop-blur-md active:bg-white/20 transition-colors border border-white/10 hover:border-white/30">
<X size={20} className="text-white/90" />
</button>
</div>
{/* Central Content (Rolling Lyrics) */}
<div className="flex-1 relative z-10 w-full overflow-hidden">
{/* Top Gradient Mask */}
<div className="absolute top-0 left-0 w-full h-24 bg-gradient-to-b from-[#2e1065]/0 to-transparent z-20 pointer-events-none"></div>
<div
className="flex flex-col items-center w-full transition-transform duration-700 ease-[cubic-bezier(0.25,1,0.5,1)]"
style={{
marginTop: 'calc(50vh - 64px)',
transform: `translateY(-${activeLineIndex * 64}px)`
}}
>
{DIALOGUE_SCRIPT.map((line, i) => {
const isActive = i === activeLineIndex;
const distance = Math.abs(activeLineIndex - i);
const isFar = distance > 2;
return (
<div
key={i}
className={`h-16 w-full px-8 flex items-center justify-center text-center transition-all duration-500 ${
isActive
? 'scale-110 opacity-100 blur-0'
: isFar
? 'opacity-10 scale-90 blur-[2px]'
: 'opacity-40 scale-95 blur-[0.5px]'
}`}
>
<p className={`font-medium leading-tight ${isActive ? 'text-white text-lg drop-shadow-[0_0_15px_rgba(192,132,252,0.6)]' : 'text-[#CBD5E1] text-base'}`}>
{line.text}
</p>
</div>
);
})}
</div>
</div>
{/* Bottom Controls & Viz - Unified Gradient */}
<div className="relative z-20 pb-12 px-0 bg-gradient-to-t from-[#2e1065] via-[#2e1065] to-transparent pt-32 -mt-32 pointer-events-none">
{/* Chart Visualizer */}
<div className="h-28 w-full mb-2 px-0 relative opacity-90">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data} margin={{ top: 10, right: 0, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="colorHz" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#C084FC" stopOpacity={0.6}/>
<stop offset="95%" stopColor="#C084FC" stopOpacity={0}/>
</linearGradient>
<linearGradient id="strokeGradient" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="#C084FC" stopOpacity={0.6} />
<stop offset="50%" stopColor="#F472B6" stopOpacity={1} />
<stop offset="100%" stopColor="#C084FC" stopOpacity={0.6} />
</linearGradient>
</defs>
<XAxis dataKey="time" type="number" domain={[0, 100]} hide />
<YAxis hide domain={[0, 100]} />
<Area
type="monotone"
dataKey="hz"
stroke="url(#strokeGradient)"
strokeWidth={3}
fillOpacity={1}
fill="url(#colorHz)"
isAnimationActive={false}
/>
<ReferenceDot
x={progress}
y={currentHz}
r={6}
fill="#F8FAFC"
stroke="#F472B6"
strokeWidth={3}
/>
</AreaChart>
</ResponsiveContainer>
</div>
{/* Controls (Pointer events enabled) */}
<div className="px-8 flex items-center gap-6 pointer-events-auto">
<button
onClick={() => setIsPlaying(!isPlaying)}
className="shrink-0 w-12 h-12 rounded-full bg-white text-[#2e1065] flex items-center justify-center shadow-[0_0_25px_rgba(255,255,255,0.3)] active:scale-95 transition-all hover:scale-105"
>
{isPlaying ? <Pause size={20} fill="currentColor" /> : <Play size={20} fill="currentColor" className="ml-0.5" />}
</button>
<div className="flex-1">
<div className="flex justify-between items-end mb-2">
<div className="flex flex-col">
<span className="text-[10px] text-[#CBD5E1] font-bold uppercase tracking-wider mb-0.5">Device Intensity</span>
<span className="text-sm text-[#F472B6] font-mono font-bold drop-shadow-sm">
: {Math.max(1, Math.ceil(progress / 10))}
</span>
</div>
<span className="text-[10px] text-[#CBD5E1] font-mono">{(progress * 1.5).toFixed(0)}s / {scenario.duration}</span>
</div>
{/* Draggable Progress Bar Container */}
<div className="relative h-6 flex items-center group">
{/* 1. Visual Track (Background) */}
<div className="absolute w-full h-1 bg-white/20 rounded-full overflow-hidden pointer-events-none backdrop-blur-sm">
{/* Visual Progress Fill */}
<div
className="h-full bg-gradient-to-r from-[#C084FC] to-[#F472B6] shadow-[0_0_15px_#F472B6]"
style={{ width: `${progress}%` }}
></div>
</div>
{/* 2. Visual Thumb (Knob) - Moves with progress */}
<div
className="absolute h-4 w-4 bg-white rounded-full shadow-[0_0_15px_rgba(255,255,255,0.8)] pointer-events-none transition-transform duration-75 border-2 border-[#F472B6]"
style={{
left: `${progress}%`,
transform: `translateX(-50%) scale(${isDragging ? 1.2 : 1})`
}}
></div>
{/* 3. Invisible Input Range (The actual interaction layer) */}
<input
type="range"
min="0"
max="100"
step="0.1"
value={progress}
onChange={handleSeek}
onMouseDown={handleDragStart}
onMouseUp={handleDragEnd}
onTouchStart={handleDragStart}
onTouchEnd={handleDragEnd}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
/>
</div>
</div>
</div>
<div className="text-center mt-8 pointer-events-auto">
<p className="text-[10px] text-[#CBD5E1]/60 font-medium tracking-widest uppercase"></p>
</div>
</div>
</div>
);
};
export default Player;

View File

@@ -0,0 +1,176 @@
import React, { useState } from 'react';
import {
ChevronLeft,
ShieldCheck,
Lock,
EyeOff,
Fingerprint,
CloudOff,
Trash2,
AlertTriangle,
History
} from 'lucide-react';
interface PrivacySafetyProps {
onBack: () => void;
}
const PrivacySafety: React.FC<PrivacySafetyProps> = ({ onBack }) => {
const [settings, setSettings] = useState({
biometric: true,
incognito: false,
cloudSync: false,
analytics: false,
blurApp: true
});
const toggleSetting = (key: keyof typeof settings) => {
setSettings(prev => ({ ...prev, [key]: !prev[key] }));
};
const handleWipeData = () => {
if(window.confirm("警告:此操作不可逆!\n\n将清除本地所有聊天记录、自定义剧本及偏好设置。\n确定执行吗")) {
alert("正在执行安全擦除...");
setTimeout(() => {
alert("数据已销毁。");
onBack();
}, 1500);
}
};
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in slide-in-from-right duration-300">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8"></div>
</div>
<div className="flex-1 overflow-y-auto no-scrollbar p-5 pb-20">
{/* Security Status Card */}
<div className="bg-[#1C1F26]/40 border border-[#10B981]/20 rounded-2xl p-6 mb-8 flex flex-col items-center justify-center relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-[#10B981]/5 to-transparent"></div>
<div className="relative z-10 w-16 h-16 rounded-full bg-[#10B981]/10 flex items-center justify-center mb-3 shadow-[0_0_20px_rgba(16,185,129,0.2)]">
<ShieldCheck size={32} className="text-[#10B981]" />
</div>
<h2 className="text-lg font-bold text-white tracking-wide"></h2>
<p className="text-xs text-[#94A3B8] mt-1"> </p>
</div>
{/* Settings Group 1: Access Control */}
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3 ml-1">访</h3>
<div className="space-y-3 mb-8">
{/* Biometric */}
<div className="bg-[#1C1F26]/60 p-4 rounded-xl flex items-center justify-between border border-white/5">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-[#8B5CF6]/10 text-[#8B5CF6]">
<Fingerprint size={20} />
</div>
<div>
<div className="text-sm font-bold text-white"></div>
<div className="text-[10px] text-[#94A3B8]">使 FaceID/ </div>
</div>
</div>
<button
onClick={() => toggleSetting('biometric')}
className={`w-11 h-6 rounded-full transition-colors relative ${settings.biometric ? 'bg-[#8B5CF6]' : 'bg-[#334155]'}`}
>
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all ${settings.biometric ? 'left-6' : 'left-1'}`}></div>
</button>
</div>
{/* App Switcher Blur */}
<div className="bg-[#1C1F26]/60 p-4 rounded-xl flex items-center justify-between border border-white/5">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-white/5 text-[#94A3B8]">
<EyeOff size={20} />
</div>
<div>
<div className="text-sm font-bold text-white"></div>
<div className="text-[10px] text-[#94A3B8]"></div>
</div>
</div>
<button
onClick={() => toggleSetting('blurApp')}
className={`w-11 h-6 rounded-full transition-colors relative ${settings.blurApp ? 'bg-[#8B5CF6]' : 'bg-[#334155]'}`}
>
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all ${settings.blurApp ? 'left-6' : 'left-1'}`}></div>
</button>
</div>
</div>
{/* Settings Group 2: Data Privacy */}
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3 ml-1"></h3>
<div className="space-y-3 mb-8">
{/* Cloud Sync */}
<div className="bg-[#1C1F26]/60 p-4 rounded-xl flex items-center justify-between border border-white/5">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-white/5 text-[#94A3B8]">
<CloudOff size={20} />
</div>
<div>
<div className="text-sm font-bold text-white"></div>
<div className="text-[10px] text-[#94A3B8]"></div>
</div>
</div>
<button
onClick={() => toggleSetting('cloudSync')}
className={`w-11 h-6 rounded-full transition-colors relative ${settings.cloudSync ? 'bg-[#8B5CF6]' : 'bg-[#334155]'}`}
>
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all ${settings.cloudSync ? 'left-6' : 'left-1'}`}></div>
</button>
</div>
{/* Incognito Mode */}
<div className="bg-[#1C1F26]/60 p-4 rounded-xl flex items-center justify-between border border-white/5">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-white/5 text-[#94A3B8]">
<Lock size={20} />
</div>
<div>
<div className="text-sm font-bold text-white"></div>
<div className="text-[10px] text-[#94A3B8]"></div>
</div>
</div>
<button
onClick={() => toggleSetting('incognito')}
className={`w-11 h-6 rounded-full transition-colors relative ${settings.incognito ? 'bg-[#8B5CF6]' : 'bg-[#334155]'}`}
>
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all ${settings.incognito ? 'left-6' : 'left-1'}`}></div>
</button>
</div>
</div>
{/* Danger Zone */}
<div className="mt-8 border-t border-white/5 pt-6">
<button
onClick={handleWipeData}
className="w-full group relative overflow-hidden rounded-xl border border-[#F43F5E]/30 bg-[#F43F5E]/5 p-4 flex items-center justify-between active:bg-[#F43F5E]/10 transition-all"
>
<div className="flex items-center gap-3 z-10">
<div className="p-2 rounded-lg bg-[#F43F5E]/10 text-[#F43F5E]">
<AlertTriangle size={20} />
</div>
<div className="text-left">
<div className="text-sm font-bold text-[#F43F5E]"></div>
<div className="text-[10px] text-[#F43F5E]/70"></div>
</div>
</div>
<Trash2 size={18} className="text-[#F43F5E] z-10" />
</button>
<div className="mt-4 flex justify-center">
<p className="text-[10px] text-[#475569] font-mono">ID: 884-291-00X-SECURE</p>
</div>
</div>
</div>
</div>
);
};
export default PrivacySafety;

View File

@@ -0,0 +1,154 @@
import React from 'react';
import {
Settings,
CreditCard,
ChevronRight,
Bluetooth,
Zap,
Crown,
Shield,
HelpCircle,
LogOut
} from 'lucide-react';
interface ProfileProps {
onTopUp: () => void;
onOpenDeviceManager: () => void;
onOpenSubscription: () => void;
onOpenPrivacy: () => void;
onOpenHelp: () => void;
onOpenSettings: () => void;
}
const Profile: React.FC<ProfileProps> = ({
onTopUp,
onOpenDeviceManager,
onOpenSubscription,
onOpenPrivacy,
onOpenHelp,
onOpenSettings
}) => {
const menuItems = [
{ id: 'device', icon: Bluetooth, label: '我的设备', sub: 'Link-X Pro' },
{ id: 'sub', icon: CreditCard, label: '订阅管理', sub: '' },
{ id: 'privacy', icon: Shield, label: '隐私安全', sub: '' },
{ id: 'help', icon: HelpCircle, label: '帮助与反馈', sub: '' },
];
const handleMenuClick = (id: string) => {
if (id === 'device') {
onOpenDeviceManager();
} else if (id === 'sub') {
onOpenSubscription();
} else if (id === 'privacy') {
onOpenPrivacy();
} else if (id === 'help') {
onOpenHelp();
}
};
return (
<div className="pt-4 px-5 pb-24 h-full overflow-y-auto no-scrollbar">
{/* 1. User Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<div className="relative">
<div className="w-18 h-18 rounded-full p-[2px] bg-gradient-to-tr from-[#C084FC] to-[#F472B6]">
<div className="w-16 h-16 rounded-full overflow-hidden border-2 border-white/20">
{/* Updated to reliable source */}
<img src="https://tse1.mm.bing.net/th?q=anime%20cool%20guy%20cyberpunk%20avatar&w=200&h=200&c=7" alt="User" className="w-full h-full object-cover" />
</div>
</div>
</div>
<div>
<h2 className="text-2xl font-bold text-white tracking-wide drop-shadow-md">Commander</h2>
<div className="flex items-center gap-1.5 mt-1 bg-white/10 backdrop-blur-md px-2.5 py-0.5 rounded-full border border-white/10 w-fit">
<Crown size={12} className="text-[#FBBF24] fill-[#FBBF24]" />
<span className="text-xs text-white font-bold tracking-wider">LV.4 </span>
</div>
</div>
</div>
<button
onClick={onOpenSettings}
className="w-10 h-10 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center text-white/70 hover:bg-white/20 hover:text-white transition-all border border-white/10"
>
<Settings size={20} />
</button>
</div>
{/* 2. Stats Grid - Lighter Glass */}
<div className="grid grid-cols-3 gap-3 mb-6">
{[
{ label: '互动时长', val: '42h', unit: '' },
{ label: '亲密指数', val: '85', unit: '%' },
{ label: '解锁剧本', val: '12', unit: '个' },
].map((stat, i) => (
<div key={i} className="glass-panel rounded-2xl p-4 flex flex-col items-center justify-center bg-white/5 border border-white/10">
<span className="text-xl font-bold text-white font-mono drop-shadow-sm">{stat.val}<span className="text-[10px] text-white/60 ml-0.5">{stat.unit}</span></span>
<span className="text-[10px] text-white/50 mt-0.5 uppercase tracking-wide">{stat.label}</span>
</div>
))}
</div>
{/* 3. VIP / Wallet Card - Ultra Vibrant */}
<div className="w-full relative h-32 rounded-3xl overflow-hidden mb-8 group active:scale-[0.99] transition-transform shadow-[0_10px_40px_-10px_rgba(192,132,252,0.5)]">
<div className="absolute inset-0 bg-gradient-to-r from-[#7C3AED] via-[#9333EA] to-[#DB2777]"></div>
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-30 mix-blend-overlay"></div>
<div className="relative z-10 p-6 flex justify-between items-center h-full">
<div className="flex flex-col justify-center">
<div className="flex items-center gap-2 mb-1 opacity-90">
<Zap size={16} fill="white" className="text-white" />
<span className="text-xs font-bold tracking-wider text-white uppercase">Joy Points</span>
</div>
<h3 className="text-4xl font-bold text-white font-mono tracking-tight drop-shadow-lg">2,450</h3>
</div>
<button
onClick={onTopUp}
className="bg-white text-[#9333EA] px-6 py-2.5 rounded-full text-sm font-bold shadow-lg active:bg-gray-100 transition-colors hover:scale-105 transform"
>
</button>
</div>
</div>
{/* 4. Menu List - Lighter Glass */}
<div className="space-y-3">
<h3 className="px-2 text-xs font-bold text-white/40 uppercase tracking-widest mb-2">General</h3>
{menuItems.map((item) => (
<button
key={item.id}
onClick={() => handleMenuClick(item.id)}
className="w-full glass-panel p-4 rounded-2xl flex items-center justify-between active:scale-[0.98] transition-all bg-white/5 hover:bg-white/10 border border-white/10 group"
>
<div className="flex items-center gap-4">
<div className="p-2.5 rounded-xl bg-white/5 text-white/70 group-hover:text-white group-hover:bg-white/20 transition-colors">
<item.icon size={20} />
</div>
<span className="text-sm text-white font-medium">{item.label}</span>
</div>
<div className="flex items-center gap-2">
{item.sub && <span className="text-xs text-white/50 font-medium">{item.sub}</span>}
<ChevronRight size={18} className="text-white/30 group-hover:text-white transition-colors" />
</div>
</button>
))}
</div>
{/* 5. System / Logout */}
<div className="mt-8 mb-4 px-2">
<button className="w-full p-4 rounded-2xl border border-red-400/30 text-red-400 flex items-center justify-center gap-2 text-sm font-medium hover:bg-red-400/10 active:scale-[0.98] transition-all bg-white/5">
<LogOut size={18} />
退
</button>
<div className="mt-6 text-center">
<p className="text-[10px] text-white/30 font-mono">Wei AI v2.5.0 (Neon Edition)</p>
</div>
</div>
</div>
);
};
export default Profile;

View File

@@ -0,0 +1,118 @@
import React, { useState } from 'react';
import {
ChevronLeft,
Bell,
Globe,
FileText,
Info,
ChevronRight,
Bot
} from 'lucide-react';
interface SettingsProps {
onBack: () => void;
}
const Settings: React.FC<SettingsProps> = ({ onBack }) => {
const [notifications, setNotifications] = useState({
push: true,
aiMsg: true
});
const toggleNotify = (key: keyof typeof notifications) => {
setNotifications(prev => ({ ...prev, [key]: !prev[key] }));
};
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in slide-in-from-right duration-300">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8"></div>
</div>
<div className="flex-1 overflow-y-auto no-scrollbar p-5 pb-20">
{/* Section 1: Notifications */}
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3 ml-1"></h3>
<div className="bg-[#1C1F26]/60 border border-white/5 rounded-xl overflow-hidden mb-6">
<div className="p-4 flex items-center justify-between border-b border-white/5">
<div className="flex items-center gap-3">
<Bell size={18} className="text-[#E2E8F0]" />
<span className="text-sm text-white"></span>
</div>
<button
onClick={() => toggleNotify('push')}
className={`w-10 h-5 rounded-full transition-colors relative ${notifications.push ? 'bg-[#8B5CF6]' : 'bg-[#334155]'}`}
>
<div className={`absolute top-1 w-3 h-3 rounded-full bg-white transition-all ${notifications.push ? 'left-6' : 'left-1'}`}></div>
</button>
</div>
<div className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<Bot size={18} className="text-[#E2E8F0]" />
<div>
<span className="text-sm text-white">AI </span>
<p className="text-[10px] text-[#64748B]"></p>
</div>
</div>
<button
onClick={() => toggleNotify('aiMsg')}
disabled={!notifications.push}
className={`w-10 h-5 rounded-full transition-colors relative ${notifications.push ? (notifications.aiMsg ? 'bg-[#8B5CF6]' : 'bg-[#334155]') : 'bg-white/5 opacity-50'}`}
>
<div className={`absolute top-1 w-3 h-3 rounded-full bg-white transition-all ${notifications.aiMsg ? 'left-6' : 'left-1'}`}></div>
</button>
</div>
</div>
{/* Section 2: App Preferences */}
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3 ml-1"></h3>
<div className="bg-[#1C1F26]/60 border border-white/5 rounded-xl overflow-hidden mb-6">
<div className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<Globe size={18} className="text-[#E2E8F0]" />
<span className="text-sm text-white"> / Language</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-[#94A3B8]"></span>
<ChevronRight size={14} className="text-[#64748B]" />
</div>
</div>
</div>
{/* Section 4: About */}
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3 ml-1"></h3>
<div className="bg-[#1C1F26]/60 border border-white/5 rounded-xl overflow-hidden mb-8">
<button className="w-full p-4 flex items-center justify-between border-b border-white/5 active:bg-white/5 transition-colors">
<div className="flex items-center gap-3">
<FileText size={18} className="text-[#E2E8F0]" />
<span className="text-sm text-white"></span>
</div>
<ChevronRight size={14} className="text-[#64748B]" />
</button>
<button className="w-full p-4 flex items-center justify-between border-b border-white/5 active:bg-white/5 transition-colors">
<div className="flex items-center gap-3">
<Info size={18} className="text-[#E2E8F0]" />
<span className="text-sm text-white"></span>
</div>
<ChevronRight size={14} className="text-[#64748B]" />
</button>
<div className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-4 h-4 rounded-full bg-[#8B5CF6] flex items-center justify-center text-[8px] font-bold text-black">V</div>
<span className="text-sm text-white"></span>
</div>
<span className="text-xs text-[#94A3B8]">v2.4.0 (8849)</span>
</div>
</div>
</div>
</div>
);
};
export default Settings;

View File

@@ -0,0 +1,175 @@
import React, { useState } from 'react';
import { ChevronLeft, Check, Crown, Zap, Star, Shield, Smartphone, Infinity } from 'lucide-react';
interface SubscriptionProps {
onBack: () => void;
}
const PLANS = [
{
id: 'month',
name: '月度协议',
price: '¥28',
period: '/月',
desc: '灵便之选,随时取消',
isPopular: false
},
{
id: 'year',
name: '年度神经连接',
price: '¥298',
period: '/年',
desc: '节省 20%,尊享全年',
originalPrice: '¥336',
isPopular: true
},
];
const PRIVILEGES = [
{ icon: Infinity, title: '剧本库无限畅玩', desc: '解锁全部付费/限定剧本' },
{ icon: Zap, title: '高频信号通道', desc: '解锁 Extreme 级震动强度' },
{ icon: Smartphone, title: '多设备同步', desc: '支持多台 Link-X 设备同时控制' },
{ icon: Star, title: 'AI 专属人格', desc: '解锁隐藏性格与深度记忆模式' },
{ icon: Crown, title: '尊贵身份标识', desc: '专属头像框与社区徽章' },
{ icon: Shield, title: '隐私加密通道', desc: '端对端加密,无痕浏览' },
];
const Subscription: React.FC<SubscriptionProps> = ({ onBack }) => {
const [selectedPlan, setSelectedPlan] = useState('year');
const [isProcessing, setIsProcessing] = useState(false);
const handleSubscribe = () => {
setIsProcessing(true);
setTimeout(() => {
setIsProcessing(false);
alert('订阅成功!欢迎加入神经连接计划。');
onBack();
}, 1500);
};
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in slide-in-from-bottom-10 duration-300 fade-in">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8"></div>
</div>
<div className="flex-1 overflow-y-auto no-scrollbar pb-32">
{/* Hero Section */}
<div className="relative h-48 overflow-hidden mb-6 shrink-0">
<div className="absolute inset-0 bg-gradient-to-br from-[#1C1F26] via-[#0F1014] to-[#0F1014]"></div>
{/* Gold Glow */}
<div className="absolute top-[-50%] left-1/2 -translate-x-1/2 w-[120%] h-full bg-[#F59E0B]/10 blur-[60px] rounded-full"></div>
<div className="relative z-10 h-full flex flex-col items-center justify-center text-center px-6">
<div className="w-12 h-12 rounded-full bg-gradient-to-tr from-[#F59E0B] to-[#FCD34D] flex items-center justify-center mb-3 shadow-[0_0_20px_rgba(245,158,11,0.3)]">
<Crown size={24} className="text-black" fill="currentColor" />
</div>
<h2 className="text-xl font-bold text-white tracking-wide"> Wei AI Pro</h2>
<p className="text-xs text-[#94A3B8] mt-2 max-w-[200px]"></p>
</div>
</div>
{/* Plan Selection */}
<div className="px-4 space-y-4 mb-8">
{PLANS.map((plan) => {
const isSelected = selectedPlan === plan.id;
return (
<div
key={plan.id}
onClick={() => setSelectedPlan(plan.id)}
className={`relative p-0.5 rounded-2xl transition-all duration-300 active:scale-[0.99] ${
isSelected
? 'bg-gradient-to-r from-[#F59E0B] to-[#FCD34D] shadow-[0_0_20px_rgba(245,158,11,0.15)]'
: 'bg-white/10'
}`}
>
<div className="bg-[#1C1F26] rounded-[14px] p-4 flex items-center justify-between relative z-10">
<div className="flex items-center gap-4">
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center transition-colors shrink-0 ${
isSelected ? 'border-[#F59E0B] bg-[#F59E0B]' : 'border-[#64748B]'
}`}>
{isSelected && <Check size={12} className="text-black stroke-[3]" />}
</div>
<div>
<div className="flex items-center gap-2">
<h3 className={`text-base font-bold ${isSelected ? 'text-white' : 'text-[#94A3B8]'}`}>{plan.name}</h3>
{plan.isPopular && (
<span className="text-[9px] font-bold bg-[#F59E0B] text-black px-1.5 py-0.5 rounded tracking-wide shrink-0">
</span>
)}
</div>
<p className="text-xs text-[#64748B] mt-1 line-clamp-1">{plan.desc}</p>
</div>
</div>
<div className="text-right shrink-0">
{plan.originalPrice && (
<span className="block text-[10px] text-[#64748B] line-through mb-0.5">{plan.originalPrice}</span>
)}
<div className="flex items-end justify-end">
<span className={`text-xl font-bold font-mono ${isSelected ? 'text-[#F59E0B]' : 'text-white'}`}>{plan.price}</span>
<span className="text-xs text-[#64748B] mb-1 ml-0.5">{plan.period}</span>
</div>
</div>
</div>
</div>
);
})}
</div>
{/* Privileges Grid */}
<div className="px-5">
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-4 flex items-center gap-2">
<Star size={12} />
</h3>
<div className="grid grid-cols-2 gap-x-4 gap-y-6">
{PRIVILEGES.map((item, idx) => (
<div key={idx} className="flex gap-3">
<div className="shrink-0 w-8 h-8 rounded-lg bg-[#F59E0B]/10 flex items-center justify-center text-[#F59E0B]">
<item.icon size={16} />
</div>
<div>
<h4 className="text-xs font-bold text-white mb-0.5">{item.title}</h4>
<p className="text-[10px] text-[#64748B] leading-tight">{item.desc}</p>
</div>
</div>
))}
</div>
</div>
{/* Terms */}
<div className="px-6 mt-8 mb-4 text-[10px] text-[#475569] text-center leading-relaxed">
<p>24</p>
<p className="mt-2"></p>
</div>
</div>
{/* Bottom Action Bar */}
<div className="absolute bottom-0 left-0 w-full p-4 bg-[#0F1014]/90 backdrop-blur-xl border-t border-white/5 z-30">
<button
onClick={handleSubscribe}
disabled={isProcessing}
className="w-full h-12 rounded-xl bg-gradient-to-r from-[#F59E0B] to-[#FBBF24] text-black font-bold tracking-wide shadow-[0_0_20px_rgba(245,158,11,0.2)] active:scale-[0.98] transition-transform flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed"
>
{isProcessing ? (
<span className="animate-pulse">...</span>
) : (
<>
<Zap size={18} fill="currentColor" />
{PLANS.find(p => p.id === selectedPlan)?.price}
</>
)}
</button>
</div>
</div>
);
};
export default Subscription;

176
wei-ai-demo/pages/TopUp.tsx Normal file
View File

@@ -0,0 +1,176 @@
import React, { useState } from 'react';
import { ChevronLeft, Zap, CreditCard, Check, ShieldCheck, Gem } from 'lucide-react';
interface TopUpProps {
onBack: () => void;
}
const RECHARGE_OPTIONS = [
{ id: 1, points: 60, price: '¥6.00', bonus: null, tag: null },
{ id: 2, points: 300, price: '¥30.00', bonus: '+15', tag: null },
{ id: 3, points: 680, price: '¥68.00', bonus: '+50', tag: '热销' },
{ id: 4, points: 1280, price: '¥128.00', bonus: '+120', tag: null },
{ id: 5, points: 3280, price: '¥328.00', bonus: '+350', tag: '超值' },
{ id: 6, points: 6480, price: '¥648.00', bonus: '+800', tag: null },
];
const PAYMENT_METHODS = [
{ id: 'alipay', name: '支付宝', icon: '支' },
{ id: 'wechat', name: '微信支付', icon: '微' },
];
const TopUp: React.FC<TopUpProps> = ({ onBack }) => {
const [selectedOption, setSelectedOption] = useState<number>(3);
const [paymentMethod, setPaymentMethod] = useState('alipay');
const [isProcessing, setIsProcessing] = useState(false);
const handlePay = () => {
setIsProcessing(true);
setTimeout(() => {
setIsProcessing(false);
alert('模拟支付成功!积分已到账。');
onBack();
}, 1500);
};
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in slide-in-from-bottom-10 duration-300 fade-in">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8"></div> {/* Spacer */}
</div>
<div className="flex-1 overflow-y-auto no-scrollbar pb-32">
{/* Current Balance Card */}
<div className="mx-4 mt-6 mb-8 relative h-32 rounded-2xl overflow-hidden shadow-[0_10px_30px_rgba(139,92,246,0.15)] group">
<div className="absolute inset-0 bg-gradient-to-br from-[#1C1F26] via-[#2D3039] to-[#1C1F26]"></div>
{/* Neon Accents */}
<div className="absolute top-0 right-0 w-32 h-32 bg-[#8B5CF6]/20 blur-[50px] rounded-full"></div>
<div className="absolute bottom-0 left-0 w-24 h-24 bg-[#F43F5E]/10 blur-[40px] rounded-full"></div>
<div className="relative z-10 p-6 flex flex-col justify-between h-full">
<div className="flex items-center gap-2 opacity-70">
<Gem size={14} className="text-[#8B5CF6]" />
<span className="text-xs font-bold text-[#E2E8F0] tracking-widest uppercase"></span>
</div>
<div className="flex items-end gap-3">
<span className="text-4xl font-bold text-white font-mono tracking-tighter drop-shadow-lg">2,450</span>
<span className="text-sm font-medium text-[#8B5CF6] mb-1.5"></span>
</div>
</div>
{/* Decorative Pattern */}
<div className="absolute right-4 top-1/2 -translate-y-1/2 opacity-5 pointer-events-none">
<Zap size={100} />
</div>
</div>
{/* Recharge Options Grid */}
<div className="px-4">
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-4 flex items-center gap-2">
<Zap size={12} />
</h3>
<div className="grid grid-cols-2 gap-3">
{RECHARGE_OPTIONS.map((opt) => {
const isSelected = selectedOption === opt.id;
return (
<button
key={opt.id}
onClick={() => setSelectedOption(opt.id)}
className={`relative p-4 rounded-xl border flex flex-col items-start transition-all duration-300 active:scale-[0.98] ${
isSelected
? 'bg-[#8B5CF6]/10 border-[#8B5CF6] shadow-[0_0_15px_rgba(139,92,246,0.15)]'
: 'bg-[#1C1F26]/60 border-white/5 hover:border-white/10'
}`}
>
{opt.tag && (
<div className={`absolute -top-2.5 -right-2 px-2 py-0.5 rounded text-[9px] font-bold tracking-wider uppercase border shadow-sm ${
opt.tag === '热销'
? 'bg-[#F43F5E] text-white border-[#F43F5E]'
: 'bg-[#F59E0B] text-black border-[#F59E0B]'
}`}>
{opt.tag}
</div>
)}
<div className="flex items-center gap-1.5 mb-1">
<Zap size={14} className={isSelected ? 'text-[#8B5CF6]' : 'text-[#64748B]'} fill={isSelected ? "currentColor" : "none"} />
<span className={`text-lg font-bold font-mono ${isSelected ? 'text-white' : 'text-[#E2E8F0]'}`}>
{opt.points}
</span>
</div>
{opt.bonus && (
<span className="text-[10px] text-[#10B981] font-mono mb-2 block">
{opt.bonus}
</span>
)}
<span className={`text-sm font-medium mt-auto ${isSelected ? 'text-[#8B5CF6]' : 'text-[#94A3B8]'}`}>
{opt.price}
</span>
</button>
);
})}
</div>
</div>
{/* Payment Method (Visual Only) */}
<div className="px-4 mt-8">
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3"></h3>
<div className="space-y-2">
{PAYMENT_METHODS.map(method => (
<button
key={method.id}
onClick={() => setPaymentMethod(method.id)}
className={`w-full flex items-center justify-between p-3 rounded-xl border transition-all ${
paymentMethod === method.id
? 'bg-[#1C1F26] border-[#8B5CF6]/50'
: 'bg-transparent border-white/5 opacity-60'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded flex items-center justify-center font-bold text-white ${method.id === 'alipay' ? 'bg-[#1677FF]' : 'bg-[#07C160]'}`}>
{method.icon}
</div>
<span className="text-sm text-white">{method.name}</span>
</div>
{paymentMethod === method.id && <Check size={16} className="text-[#8B5CF6]" />}
</button>
))}
</div>
</div>
{/* Terms */}
<div className="px-6 mt-8 mb-4 flex items-start gap-2 text-[10px] text-[#64748B] leading-tight">
<ShieldCheck size={12} className="shrink-0 mt-0.5" />
<p>退</p>
</div>
</div>
{/* Bottom Action Bar */}
<div className="absolute bottom-0 left-0 w-full p-4 bg-[#0F1014]/90 backdrop-blur-xl border-t border-white/5 z-30">
<button
onClick={handlePay}
disabled={isProcessing}
className="w-full h-12 rounded-xl bg-gradient-to-r from-[#8B5CF6] to-[#6366f1] text-white font-bold tracking-wide shadow-[0_0_20px_rgba(139,92,246,0.3)] active:scale-[0.98] transition-transform flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed"
>
{isProcessing ? (
<span className="animate-pulse">...</span>
) : (
<>
<CreditCard size={18} />
{RECHARGE_OPTIONS.find(o => o.id === selectedOption)?.price}
</>
)}
</button>
</div>
</div>
);
};
export default TopUp;

29
wei-ai-demo/tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

47
wei-ai-demo/types.ts Normal file
View File

@@ -0,0 +1,47 @@
export interface Message {
id: string;
text: string;
sender: 'user' | 'ai';
type: 'text' | 'image' | 'audio';
timestamp: number;
imageUrl?: string;
isLocked?: boolean;
}
export interface Scenario {
id: string;
title: string;
category: string;
cover: string;
duration: string; // e.g., "15:00"
intensity: 'Low' | 'Medium' | 'High' | 'Extreme';
isLocked: boolean;
tags: string[];
}
export interface Character {
id: string;
name: string;
tagline: string;
avatar: string;
description: string;
tags: string[];
compatibility: number; // 硬件契合度 %
status: 'online' | 'busy' | 'offline';
isLocked?: boolean;
}
export interface DeviceStatus {
connected: boolean;
battery: number;
temperature: number;
signalStrength: number;
currentMode: 'Idle' | 'Pattern' | 'Manual';
}
export enum UserTab {
Discovery = 'discovery', // Changed from Interaction
Library = 'library',
Control = 'control',
Profile = 'profile',
}

View File

@@ -0,0 +1,23 @@
import path from 'path';
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
server: {
port: 3000,
host: '0.0.0.0',
},
plugins: [react()],
define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
}
}
};
});

45
wei_ai_app/.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
wei_ai_app/.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "19074d12f7eaf6a8180cd4036a430c1d76de904e"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
- platform: macos
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

16
wei_ai_app/README.md Normal file
View File

@@ -0,0 +1,16 @@
# wei_ai_app
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
wei_ai_app/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.weiai.wei_ai_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.weiai.wei_ai_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="wei_ai_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.weiai.wei_ai_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

34
wei_ai_app/ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
wei_ai_app/ios/Podfile Normal file
View File

@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View File

@@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.weiai.weiAiApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.weiai.weiAiApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.weiai.weiAiApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.weiai.weiAiApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.weiai.weiAiApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.weiai.weiAiApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Wei Ai App</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>wei_ai_app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppTheme {
// Cyberpunk Color Palette
static const Color neonBlue = Color(0xFF00F0FF);
static const Color neonPurple = Color(0xFFBC13FE);
static const Color neonGreen = Color(0xFF0AFF99);
static const Color darkBg = Color(0xFF050510);
static const Color darkSurface = Color(0xFF13132B);
static const Color textPrimary = Color(0xFFE0E0FF);
static const Color textSecondary = Color(0xFFA0A0C0);
static final ThemeData darkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
scaffoldBackgroundColor: darkBg,
// Typography
textTheme: GoogleFonts.outfitTextTheme(ThemeData.dark().textTheme).copyWith(
displayLarge: const TextStyle(color: textPrimary, fontWeight: FontWeight.bold),
displayMedium: const TextStyle(color: textPrimary, fontWeight: FontWeight.bold),
bodyLarge: const TextStyle(color: textPrimary),
bodyMedium: const TextStyle(color: textSecondary),
),
// Color Scheme
colorScheme: const ColorScheme.dark(
primary: neonBlue,
secondary: neonPurple,
surface: darkBg, // Use darkBg as surface or stick to darkSurface? Let's use darkBg here as background replacement
error: Color(0xFFFF2A6D),
onPrimary: Colors.black,
onSecondary: Colors.white,
onSurface: textPrimary,
),
// Component Themes
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: textPrimary),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: darkSurface,
selectedItemColor: neonBlue,
unselectedItemColor: textSecondary,
type: BottomNavigationBarType.fixed,
showSelectedLabels: true,
showUnselectedLabels: true,
),
iconTheme: const IconThemeData(
color: neonBlue,
),
);
}

View File

@@ -0,0 +1,150 @@
import '../models/character.dart';
import '../models/message.dart';
import '../models/scenario.dart';
String _getImg(String keyword) =>
'https://tse1.mm.bing.net/th?q=${Uri.encodeComponent(keyword)}&w=600&h=900&c=7&rs=1&p=0&dpr=2&pid=1.7&mkt=en-US&adlt=moderate';
final List<Message> mockMessages = [
Message(
id: '1',
text: '连接已建立,正在校准生物反馈信号...',
sender: MessageSender.ai,
type: MessageType.text,
timestamp: DateTime.now().subtract(const Duration(minutes: 5)),
),
Message(
id: '2',
text: '检测到心率略有上升,你需要我的安抚吗?',
sender: MessageSender.ai,
type: MessageType.text,
timestamp: DateTime.now().subtract(const Duration(minutes: 4)),
),
];
final List<Character> mockCharacters = [
Character(
id: 'c1',
name: 'Eva-09',
tagline: '私人仿生护理专员',
avatar: _getImg('anime girl white bikini silver hair gentle portrait masterpiece'),
description: '专为高压人群设计的仿生人型号,擅长通过精准的触觉反馈缓解神经紧张。',
tags: ['温顺', '医疗', '治愈'],
compatibility: 98,
status: 'online',
),
Character(
id: 'c2',
name: 'Commander V',
tagline: '深空舰队指挥官',
avatar: _getImg('anime girl black bikini military cap domineering expression dark hair'),
description: '性格强势,喜欢掌控一切。在连接中,你需要完全服从她的指令。',
tags: ['强势', '指令', '调教'],
compatibility: 85,
status: 'online',
),
Character(
id: 'c3',
name: 'Yuki (故障版)',
tagline: '觉醒的虚拟偶像',
avatar: _getImg('anime girl pink bikini cyberpunk neon colorful hair yandere'),
description: '核心代码出现异常逻辑,表现出极强的占有欲和不可预测的信号波动。',
tags: ['病娇', '不稳定', '高频'],
compatibility: 92,
status: 'busy',
),
Character(
id: 'c4',
name: 'Secret X',
tagline: '未知信号源',
avatar: _getImg('anime girl purple micro bikini mysterious dark glowing eyes sexy'),
description: '权限不足,请提升会员等级以解码该信号源。',
tags: ['神秘', '极乐'],
compatibility: 0,
status: 'offline',
isLocked: true,
),
];
// Scenario cover helper
String _getCover(String keyword) =>
'https://tse1.mm.bing.net/th?q=${Uri.encodeComponent(keyword)}&w=400&h=600&c=7&rs=1&p=0&dpr=2&pid=1.7&mkt=en-US&adlt=moderate';
// Mock Scenarios for Library
final List<Scenario> mockScenarios = [
Scenario(
id: '1',
title: '午夜办公室的加班',
category: '职场',
cover: _getCover('anime girl office lady lingerie night city window'),
duration: '12:30',
intensity: 'Medium',
isLocked: false,
tags: ['沉浸', 'ASMR'],
),
Scenario(
id: '2',
title: '私人医生的检查',
category: '角色扮演',
cover: _getCover('anime nurse girl white bikini hospital room'),
duration: '18:00',
intensity: 'High',
isLocked: true,
tags: ['强互动', '语音'],
),
Scenario(
id: '3',
title: '海边度假的偶遇',
category: '邻家',
cover: _getCover('anime girl blue bikini running beach ocean sunny'),
duration: '25:00',
intensity: 'Low',
isLocked: false,
tags: ['纯爱', '剧情'],
),
Scenario(
id: '4',
title: '赛博仿生人测试',
category: '科幻',
cover: _getCover('anime cyborg girl metallic bikini sci-fi lab wires'),
duration: '10:00',
intensity: 'Extreme',
isLocked: true,
tags: ['硬核', '指令'],
),
Scenario(
id: '5',
title: '深夜电台主播',
category: 'ASMR',
cover: _getCover('anime girl headphones microphone studio night'),
duration: '15:00',
intensity: 'Low',
isLocked: false,
tags: ['ASMR', '治愈'],
),
];
// Dialogue script synced with progress (0-100)
class DialogueLine {
final double time; // 0-100 progress percentage
final String text;
const DialogueLine({required this.time, required this.text});
}
const List<DialogueLine> dialogueScript = [
DialogueLine(time: 0, text: '正在建立神经连接...'),
DialogueLine(time: 5, text: '(检测到心率轻微上升)'),
DialogueLine(time: 12, text: '"放松,把控制权交给我。"'),
DialogueLine(time: 20, text: '"很好,保持呼吸频率..."'),
DialogueLine(time: 28, text: '正在启动触觉反馈模块'),
DialogueLine(time: 35, text: '"感觉到那个节奏了吗?"'),
DialogueLine(time: 45, text: '强度正在逐渐增加...'),
DialogueLine(time: 55, text: '"不要抵抗,顺从它。"'),
DialogueLine(time: 65, text: '"我会稍微加快一点速度。"'),
DialogueLine(time: 75, text: '(设备输出功率提升至 80%)'),
DialogueLine(time: 85, text: '"就是现在..."'),
DialogueLine(time: 92, text: '"做得很好,指挥官。"'),
DialogueLine(time: 100, text: '连接结束。'),
];

22
wei_ai_app/lib/main.dart Normal file
View File

@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'config/theme.dart';
import 'router/app_router.dart';
void main() {
runApp(const ProviderScope(child: WeiAiApp()));
}
class WeiAiApp extends StatelessWidget {
const WeiAiApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Wei AI - Cyber Space',
theme: AppTheme.darkTheme,
routerConfig: appRouter,
debugShowCheckedModeBanner: false,
);
}
}

View File

@@ -0,0 +1,23 @@
class Character {
final String id;
final String name;
final String tagline;
final String avatar;
final String description;
final List<String> tags;
final double compatibility; // 硬件契合度 %
final String status; // 'online' | 'busy' | 'offline'
final bool isLocked;
const Character({
required this.id,
required this.name,
required this.tagline,
required this.avatar,
required this.description,
required this.tags,
required this.compatibility,
required this.status,
this.isLocked = false,
});
}

View File

@@ -0,0 +1,33 @@
enum DeviceMode { idle, pattern, manual }
class DeviceStatus {
final bool connected;
final double battery;
final double temperature;
final int signalStrength;
final DeviceMode currentMode;
const DeviceStatus({
this.connected = false,
this.battery = 100.0,
this.temperature = 36.5,
this.signalStrength = 0,
this.currentMode = DeviceMode.idle,
});
DeviceStatus copyWith({
bool? connected,
double? battery,
double? temperature,
int? signalStrength,
DeviceMode? currentMode,
}) {
return DeviceStatus(
connected: connected ?? this.connected,
battery: battery ?? this.battery,
temperature: temperature ?? this.temperature,
signalStrength: signalStrength ?? this.signalStrength,
currentMode: currentMode ?? this.currentMode,
);
}
}

View File

@@ -0,0 +1,22 @@
enum MessageType { text, image, audio }
enum MessageSender { user, ai }
class Message {
final String id;
final String text;
final MessageSender sender;
final MessageType type;
final DateTime timestamp;
final String? imageUrl;
final bool isLocked;
const Message({
required this.id,
required this.text,
required this.sender,
required this.type,
required this.timestamp,
this.imageUrl,
this.isLocked = false,
});
}

View File

@@ -0,0 +1,21 @@
class Scenario {
final String id;
final String title;
final String category;
final String cover;
final String duration;
final String intensity; // 'Low' | 'Medium' | 'High' | 'Extreme'
final bool isLocked;
final List<String> tags;
const Scenario({
required this.id,
required this.title,
required this.category,
required this.cover,
required this.duration,
required this.intensity,
required this.isLocked,
required this.tags,
});
}

View File

@@ -0,0 +1,33 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/device_status.dart';
class DeviceNotifier extends Notifier<DeviceStatus> {
Timer? _timer;
@override
DeviceStatus build() {
_startSimulation();
ref.onDispose(() {
_timer?.cancel();
});
return const DeviceStatus(connected: true, battery: 82.0);
}
void _startSimulation() {
_timer = Timer.periodic(const Duration(seconds: 5), (timer) {
if (state.battery > 0) {
state = state.copyWith(
battery: state.connected ? state.battery - 0.05 : state.battery,
signalStrength: state.connected ? (85 + (timer.tick % 10)).toInt() : 0,
);
}
});
}
void toggleConnection() {
state = state.copyWith(connected: !state.connected);
}
}
final deviceProvider = NotifierProvider<DeviceNotifier, DeviceStatus>(DeviceNotifier.new);

View File

@@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../screens/main/main_screen.dart';
import '../screens/discovery/discovery_screen.dart';
import '../screens/library/library_screen.dart';
import '../screens/control/control_screen.dart';
import '../screens/control/free_control_screen.dart';
import '../screens/control/pattern_control_screen.dart';
import '../screens/profile/profile_screen.dart';
import '../screens/profile/settings_screen.dart';
import '../screens/profile/topup_screen.dart';
import '../screens/profile/device_manager_screen.dart';
import '../screens/profile/subscription_screen.dart';
import '../screens/profile/privacy_screen.dart';
import '../screens/profile/help_screen.dart';
import '../screens/interaction/interaction_screen.dart';
import '../screens/player/script_player_screen.dart';
// Private navigators
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorDiscoveryKey = GlobalKey<NavigatorState>(debugLabel: 'shellDiscovery');
final _shellNavigatorLibraryKey = GlobalKey<NavigatorState>(debugLabel: 'shellLibrary');
final _shellNavigatorControlKey = GlobalKey<NavigatorState>(debugLabel: 'shellControl');
final _shellNavigatorProfileKey = GlobalKey<NavigatorState>(debugLabel: 'shellProfile');
final appRouter = GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/discovery',
routes: [
// Top-level route for Interaction to cover BottomNav
GoRoute(
path: '/interaction/:characterId',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) {
final characterId = state.pathParameters['characterId']!;
return InteractionScreen(characterId: characterId);
},
),
// Top-level route for Script Player to cover BottomNav
GoRoute(
path: '/player/:scenarioId',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) {
final scenarioId = state.pathParameters['scenarioId']!;
return ScriptPlayerScreen(scenarioId: scenarioId);
},
),
// Top-level route for Free Control to cover BottomNav
GoRoute(
path: '/control/free',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const FreeControlScreen(),
),
// Top-level route for Pattern Control to cover BottomNav
GoRoute(
path: '/control/pattern',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const PatternControlScreen(),
),
// Profile sub-pages
GoRoute(
path: '/profile/settings',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const SettingsScreen(),
),
GoRoute(
path: '/profile/topup',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const TopupScreen(),
),
GoRoute(
path: '/profile/device',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const DeviceManagerScreen(),
),
GoRoute(
path: '/profile/subscription',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const SubscriptionScreen(),
),
GoRoute(
path: '/profile/privacy',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const PrivacyScreen(),
),
GoRoute(
path: '/profile/help',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const HelpScreen(),
),
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return MainScreen(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
navigatorKey: _shellNavigatorDiscoveryKey,
routes: [
GoRoute(
path: '/discovery',
builder: (context, state) => const DiscoveryScreen(),
),
],
),
StatefulShellBranch(
navigatorKey: _shellNavigatorLibraryKey,
routes: [
GoRoute(
path: '/library',
builder: (context, state) => const LibraryScreen(),
),
],
),
StatefulShellBranch(
navigatorKey: _shellNavigatorControlKey,
routes: [
GoRoute(
path: '/control',
builder: (context, state) => const ControlScreen(),
),
],
),
StatefulShellBranch(
navigatorKey: _shellNavigatorProfileKey,
routes: [
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
],
),
],
),
],
);

View File

@@ -0,0 +1,315 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../widgets/tab_content_layout.dart';
enum DeviceState { disconnected, connecting, connected }
class ControlScreen extends StatefulWidget {
const ControlScreen({super.key});
@override
State<ControlScreen> createState() => _ControlScreenState();
}
class _ControlScreenState extends State<ControlScreen> {
DeviceState _deviceStatus = DeviceState.disconnected;
void _connectDevice() {
setState(() => _deviceStatus = DeviceState.connecting);
Future.delayed(const Duration(milliseconds: 1500), () {
if (mounted) setState(() => _deviceStatus = DeviceState.connected);
});
}
void _disconnectDevice() {
setState(() => _deviceStatus = DeviceState.disconnected);
}
@override
Widget build(BuildContext context) {
const double bottomNavHeight = 90;
return TabContentLayout(
child: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(16, 8, 16, bottomNavHeight + 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Device Card
_DeviceCard(
status: _deviceStatus,
onConnect: _connectDevice,
onDisconnect: _disconnectDevice,
),
const SizedBox(height: 16),
// Mode Selection Title
Text(
'操控模式',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
letterSpacing: 2,
color: Colors.white.withOpacity(0.4),
),
),
const SizedBox(height: 12),
// Free Control Button
_ModeButton(
title: '自由操控',
subtitle: '指尖滑动控制 • 实时反馈',
icon: LucideIcons.sliders,
iconColor: const Color(0xFFC084FC),
enabled: _deviceStatus == DeviceState.connected,
onTap: () => context.push('/control/free'),
),
const SizedBox(height: 12),
// Pattern Control Button
_ModeButton(
title: '波形模式',
subtitle: '6种预设震动韵律',
icon: LucideIcons.waves,
iconColor: const Color(0xFF60A5FA),
enabled: _deviceStatus == DeviceState.connected,
onTap: () => context.push('/control/pattern'),
),
],
),
),
);
}
}
// Device Card Widget
class _DeviceCard extends StatelessWidget {
final DeviceState status;
final VoidCallback onConnect;
final VoidCallback onDisconnect;
const _DeviceCard({
required this.status,
required this.onConnect,
required this.onDisconnect,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withOpacity(0.2)),
),
child: Stack(
children: [
// Background Icon
Positioned(
right: -20,
top: -20,
child: Icon(
LucideIcons.bluetooth,
size: 100,
color: Colors.white.withOpacity(0.05),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'Link-X Pro',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
if (status == DeviceState.connected) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF34D399).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF34D399).withOpacity(0.3)),
),
child: const Text(
'ONLINE',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Color(0xFF34D399),
),
),
),
],
],
),
const SizedBox(height: 4),
Text(
status == DeviceState.disconnected
? '设备未连接'
: status == DeviceState.connecting
? '正在搜索信号...'
: 'ID: 884-X9-01',
style: TextStyle(
fontSize: 12,
fontFamily: 'monospace',
color: Colors.white.withOpacity(0.6),
),
),
],
),
// Connect Button
GestureDetector(
onTap: status == DeviceState.connected ? onDisconnect : onConnect,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: status == DeviceState.connected
? Colors.white.withOpacity(0.1)
: status == DeviceState.connecting
? Colors.white.withOpacity(0.2)
: Colors.white,
borderRadius: BorderRadius.circular(20),
border: status == DeviceState.connected
? Border.all(color: Colors.white.withOpacity(0.1))
: null,
),
child: Text(
status == DeviceState.connected
? '断开'
: status == DeviceState.connecting
? '连接中'
: '连接设备',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: status == DeviceState.connected
? Colors.white.withOpacity(0.7)
: const Color(0xFF2E1065),
),
),
),
),
],
),
// Battery Info
if (status == DeviceState.connected) ...[
const SizedBox(height: 16),
Row(
children: [
const Icon(LucideIcons.battery, size: 16, color: Color(0xFF34D399)),
const SizedBox(width: 8),
const Text(
'85%',
style: TextStyle(
fontSize: 14,
fontFamily: 'monospace',
color: Colors.white,
),
),
],
),
],
],
),
],
),
);
}
}
// Mode Button Widget
class _ModeButton extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final Color iconColor;
final bool enabled;
final VoidCallback onTap;
const _ModeButton({
required this.title,
required this.subtitle,
required this.icon,
required this.iconColor,
required this.enabled,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: enabled ? onTap : null,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: enabled ? 1.0 : 0.5,
child: Container(
height: 120,
padding: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: Colors.white.withOpacity(enabled ? 0.1 : 0.05),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Colors.white.withOpacity(enabled ? 0.2 : 0.05),
),
),
child: Row(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: iconColor.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(icon, size: 28, color: iconColor),
),
const SizedBox(width: 20),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.5),
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,205 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lucide_icons/lucide_icons.dart';
class FreeControlScreen extends StatefulWidget {
const FreeControlScreen({super.key});
@override
State<FreeControlScreen> createState() => _FreeControlScreenState();
}
class _FreeControlScreenState extends State<FreeControlScreen> {
double _intensity = 0;
bool _isClimax = false;
void _handleInteraction(Offset localPosition, double height) {
final relativeY = 1 - (localPosition.dy / height).clamp(0.0, 1.0);
setState(() => _intensity = (relativeY * 100).roundToDouble());
}
void _handleClimax() {
if (_isClimax) return;
setState(() {
_isClimax = true;
_intensity = 100;
});
HapticFeedback.heavyImpact();
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
setState(() {
_isClimax = false;
_intensity = 20;
});
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF2E1065),
body: SafeArea(
child: Column(
children: [
// Header
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 16, 16),
child: Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(LucideIcons.chevronLeft, color: Colors.white.withOpacity(0.7)),
),
const Text(
'自由操控',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
// Control Area
Expanded(
child: Center(
child: LayoutBuilder(
builder: (context, constraints) {
final controlHeight = constraints.maxHeight * 0.65;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onPanUpdate: (details) {
_handleInteraction(details.localPosition, controlHeight);
},
onTapDown: (details) {
_handleInteraction(details.localPosition, controlHeight);
},
child: Container(
width: 180,
height: controlHeight,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(32),
border: Border.all(color: Colors.white.withOpacity(0.2)),
),
child: Stack(
children: [
// Fill Level
Align(
alignment: Alignment.bottomCenter,
child: AnimatedContainer(
duration: const Duration(milliseconds: 50),
width: double.infinity,
height: controlHeight * (_intensity / 100),
decoration: BoxDecoration(
color: _isClimax
? Colors.red.withOpacity(0.5)
: const Color(0xFFC084FC).withOpacity(0.5),
borderRadius: BorderRadius.circular(32),
),
),
),
// Labels
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
'MAX',
style: TextStyle(
fontSize: 10,
fontFamily: 'monospace',
color: Colors.white.withOpacity(0.4),
),
),
),
Center(
child: Text(
_intensity.toInt().toString(),
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
color: _isClimax ? Colors.red[200] : Colors.white,
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
'OFF',
style: TextStyle(
fontSize: 10,
fontFamily: 'monospace',
color: Colors.white.withOpacity(0.4),
),
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
Text(
'上下滑动触控板以控制强度',
style: TextStyle(
fontSize: 12,
fontFamily: 'monospace',
letterSpacing: 1,
color: Colors.white.withOpacity(0.6),
),
),
],
);
},
),
),
),
// Climax Button
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 40),
child: GestureDetector(
onTap: _handleClimax,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 18),
decoration: BoxDecoration(
color: _isClimax ? Colors.red : Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: _isClimax ? Colors.red : Colors.red.withOpacity(0.5),
),
),
child: Center(
child: Text(
_isClimax ? 'MAX OUTPUT...' : '一键爆发',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 2,
color: _isClimax ? Colors.white : Colors.red[300],
),
),
),
),
),
),
],
),
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More