Compare commits
3 Commits
c00a8c6ff6
...
65cce4868d
| Author | SHA1 | Date | |
|---|---|---|---|
| 65cce4868d | |||
| 53ed4a7ca5 | |||
| c750bd4c94 |
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = function (api) {
|
||||||
|
api.cache(true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
presets: [["babel-preset-expo", { jsxImportSource: "nativewind" }], "nativewind/babel"],
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// https://docs.expo.dev/guides/using-eslint/
|
||||||
|
const { defineConfig } = require("eslint/config");
|
||||||
|
const expoConfig = require("eslint-config-expo/flat");
|
||||||
|
|
||||||
|
module.exports = defineConfig([
|
||||||
|
expoConfig,
|
||||||
|
{
|
||||||
|
ignores: ["dist/*"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
+9
-8
@@ -1,20 +1,21 @@
|
|||||||
const path = require('path');
|
const path = require("path");
|
||||||
const { getDefaultConfig } = require('expo/metro-config');
|
const { getDefaultConfig } = require("expo/metro-config");
|
||||||
|
const { withNativeWind } = require("nativewind/metro");
|
||||||
|
|
||||||
const projectRoot = __dirname;
|
const projectRoot = __dirname;
|
||||||
const workspaceRoot = path.resolve(projectRoot, '..');
|
const workspaceRoot = path.resolve(projectRoot, "..");
|
||||||
|
|
||||||
const config = getDefaultConfig(projectRoot);
|
const config = getDefaultConfig(projectRoot);
|
||||||
|
|
||||||
config.watchFolders = [workspaceRoot];
|
config.watchFolders = [workspaceRoot];
|
||||||
config.resolver.alias = {
|
config.resolver.alias = {
|
||||||
...config.resolver.alias,
|
...config.resolver.alias,
|
||||||
'@infiplot/core': path.resolve(workspaceRoot, 'packages/core/src/index.ts'),
|
"@infiplot/core": path.resolve(workspaceRoot, "packages/core/src/index.ts"),
|
||||||
'@infiplot/types': path.resolve(workspaceRoot, 'packages/types/src/index.ts'),
|
"@infiplot/types": path.resolve(workspaceRoot, "packages/types/src/index.ts"),
|
||||||
};
|
};
|
||||||
config.resolver.nodeModulesPaths = [
|
config.resolver.nodeModulesPaths = [
|
||||||
path.resolve(projectRoot, 'node_modules'),
|
path.resolve(projectRoot, "node_modules"),
|
||||||
path.resolve(workspaceRoot, 'node_modules'),
|
path.resolve(workspaceRoot, "node_modules"),
|
||||||
];
|
];
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = withNativeWind(config, { input: "./src/global.css" });
|
||||||
|
|||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="nativewind/types" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.
|
||||||
+7
-1
@@ -19,18 +19,24 @@
|
|||||||
"expo-symbols": "~56.0.6",
|
"expo-symbols": "~56.0.6",
|
||||||
"expo-system-ui": "~56.0.5",
|
"expo-system-ui": "~56.0.5",
|
||||||
"expo-web-browser": "~56.0.5",
|
"expo-web-browser": "~56.0.5",
|
||||||
|
"nativewind": "^4.2.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-native": "0.85.3",
|
"react-native": "0.85.3",
|
||||||
|
"react-native-css-interop": "^0.2.6",
|
||||||
"react-native-gesture-handler": "~2.31.1",
|
"react-native-gesture-handler": "~2.31.1",
|
||||||
"react-native-reanimated": "4.3.1",
|
"react-native-reanimated": "4.3.1",
|
||||||
"react-native-safe-area-context": "~5.7.0",
|
"react-native-safe-area-context": "~5.7.0",
|
||||||
"react-native-screens": "4.25.2",
|
"react-native-screens": "4.25.2",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
"react-native-worklets": "0.8.3"
|
"react-native-worklets": "0.8.3",
|
||||||
|
"tailwindcss": "^3.4.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.2.2",
|
"@types/react": "~19.2.2",
|
||||||
|
"eslint": "^9.0.0",
|
||||||
|
"eslint-config-expo": "~56.0.4",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.8.0",
|
||||||
"typescript": "~6.0.3"
|
"typescript": "~6.0.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DarkTheme, DefaultTheme, ThemeProvider } from 'expo-router';
|
import { DarkTheme, DefaultTheme, ThemeProvider } from 'expo-router';
|
||||||
import { useColorScheme } from 'react-native';
|
import { useColorScheme } from 'react-native';
|
||||||
|
|
||||||
|
import '@/global.css';
|
||||||
import { AnimatedSplashOverlay } from '@/components/animated-icon';
|
import { AnimatedSplashOverlay } from '@/components/animated-icon';
|
||||||
import AppTabs from '@/components/app-tabs';
|
import AppTabs from '@/components/app-tabs';
|
||||||
|
|
||||||
|
|||||||
+152
-284
@@ -1,17 +1,10 @@
|
|||||||
import { creationOptions, defaultStoryDraft } from '@infiplot/core';
|
import { creationOptions, defaultStoryDraft } from "@infiplot/core";
|
||||||
import { SymbolView } from 'expo-symbols';
|
import { SymbolView } from "expo-symbols";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import {
|
import { Pressable, ScrollView, Text, TextInput, View } from "react-native";
|
||||||
Pressable,
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
ScrollView,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
View,
|
|
||||||
} from 'react-native';
|
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
import { BottomTabInset } from '@/constants/theme';
|
import { BottomTabInset } from "@/constants/theme";
|
||||||
|
|
||||||
export default function CreateScreen() {
|
export default function CreateScreen() {
|
||||||
const [world, setWorld] = useState(defaultStoryDraft.worldSetting);
|
const [world, setWorld] = useState(defaultStoryDraft.worldSetting);
|
||||||
@@ -23,92 +16,157 @@ export default function CreateScreen() {
|
|||||||
const ready = world.trim().length > 10 && style.trim().length > 5;
|
const ready = world.trim().length > 10 && style.trim().length > 5;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe}>
|
<SafeAreaView className="flex-1 bg-[#09090b]">
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scroll}
|
className="flex-1"
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
contentContainerStyle={styles.content}>
|
contentContainerStyle={{ paddingBottom: BottomTabInset + 112 }}
|
||||||
<View style={styles.header}>
|
>
|
||||||
<View>
|
<View className="gap-[18px] p-[18px]">
|
||||||
<Text style={styles.eyebrow}>AI 剧情创作</Text>
|
<View className="flex-row items-start justify-between gap-[18px] pt-2">
|
||||||
<Text style={styles.title}>写下两段文字,生成可刷的短剧。</Text>
|
<View>
|
||||||
|
<Text className="mb-2 text-xs font-black text-[#ff4d6d]">
|
||||||
|
AI 剧情创作
|
||||||
|
</Text>
|
||||||
|
<Text className="max-w-[300px] text-[30px] font-black leading-9 text-white">
|
||||||
|
写下两段文字,生成可刷的短剧。
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View className="h-[46px] w-[46px] items-center justify-center rounded-full bg-white/10">
|
||||||
|
<SymbolView
|
||||||
|
name={{
|
||||||
|
ios: "sparkles",
|
||||||
|
android: "auto_awesome",
|
||||||
|
web: "auto_awesome",
|
||||||
|
}}
|
||||||
|
size={24}
|
||||||
|
tintColor="#fff"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.headerIcon}>
|
|
||||||
<SymbolView name={{ ios: 'sparkles', android: 'auto_awesome', web: 'auto_awesome' }} size={24} tintColor="#fff" />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.section}>
|
<View className="gap-2.5">
|
||||||
<Text style={styles.label}>题材模板</Text>
|
<Text className="text-[13px] font-black text-white/70">
|
||||||
<ChipRow options={creationOptions.templates} value={template} onChange={setTemplate} />
|
题材模板
|
||||||
</View>
|
</Text>
|
||||||
|
<ChipRow
|
||||||
<View style={styles.fieldBlock}>
|
options={creationOptions.templates}
|
||||||
<View style={styles.fieldHeader}>
|
value={template}
|
||||||
<Text style={styles.label}>世界设定</Text>
|
onChange={setTemplate}
|
||||||
<Text style={styles.counter}>{world.length}</Text>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<TextInput
|
|
||||||
value={world}
|
|
||||||
onChangeText={setWorld}
|
|
||||||
multiline
|
|
||||||
textAlignVertical="top"
|
|
||||||
placeholder="主角、世界、冲突、第一幕钩子..."
|
|
||||||
placeholderTextColor="rgba(255,255,255,0.36)"
|
|
||||||
style={styles.textArea}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.fieldBlock}>
|
<View className="gap-2.5">
|
||||||
<View style={styles.fieldHeader}>
|
<View className="flex-row items-center justify-between">
|
||||||
<Text style={styles.label}>视觉风格</Text>
|
<Text className="text-[13px] font-black text-white/70">
|
||||||
<Text style={styles.counter}>{style.length}</Text>
|
世界设定
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs font-extrabold text-white/40">
|
||||||
|
{world.length}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<TextInput
|
||||||
|
value={world}
|
||||||
|
onChangeText={setWorld}
|
||||||
|
multiline
|
||||||
|
textAlignVertical="top"
|
||||||
|
placeholder="主角、世界、冲突、第一幕钩子..."
|
||||||
|
placeholderTextColor="rgba(255,255,255,0.36)"
|
||||||
|
className="min-h-[158px] rounded-lg border border-white/10 bg-white/10 p-3.5 text-base font-semibold leading-6 text-white"
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<TextInput
|
|
||||||
value={style}
|
|
||||||
onChangeText={setStyle}
|
|
||||||
multiline
|
|
||||||
textAlignVertical="top"
|
|
||||||
placeholder="镜头、色彩、材质、画风、氛围..."
|
|
||||||
placeholderTextColor="rgba(255,255,255,0.36)"
|
|
||||||
style={[styles.textArea, styles.shortArea]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.twoColumns}>
|
<View className="gap-2.5">
|
||||||
<View style={styles.optionCard}>
|
<View className="flex-row items-center justify-between">
|
||||||
<Text style={styles.label}>画面比例</Text>
|
<Text className="text-[13px] font-black text-white/70">
|
||||||
<ChipRow compact options={creationOptions.ratios} value={ratio} onChange={setRatio} />
|
视觉风格
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs font-extrabold text-white/40">
|
||||||
|
{style.length}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<TextInput
|
||||||
|
value={style}
|
||||||
|
onChangeText={setStyle}
|
||||||
|
multiline
|
||||||
|
textAlignVertical="top"
|
||||||
|
placeholder="镜头、色彩、材质、画风、氛围..."
|
||||||
|
placeholderTextColor="rgba(255,255,255,0.36)"
|
||||||
|
className="min-h-[116px] rounded-lg border border-white/10 bg-white/10 p-3.5 text-base font-semibold leading-6 text-white"
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.optionCard}>
|
|
||||||
<Text style={styles.label}>叙事节奏</Text>
|
|
||||||
<ChipRow compact options={creationOptions.rhythms} value={rhythm} onChange={setRhythm} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.previewCard}>
|
<View className="flex-row gap-3">
|
||||||
<Text style={styles.previewTitle}>生成流程</Text>
|
<View className="flex-1 gap-3 rounded-lg bg-white/[0.07] p-3">
|
||||||
<View style={styles.steps}>
|
<Text className="text-[13px] font-black text-white/70">
|
||||||
{['故事圣经', '分镜脚本', '角色视觉', '首集预览'].map((step, index) => (
|
画面比例
|
||||||
<View key={step} style={styles.step}>
|
</Text>
|
||||||
<Text style={styles.stepIndex}>{index + 1}</Text>
|
<ChipRow
|
||||||
<Text style={styles.stepText}>{step}</Text>
|
compact
|
||||||
</View>
|
options={creationOptions.ratios}
|
||||||
))}
|
value={ratio}
|
||||||
|
onChange={setRatio}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View className="flex-1 gap-3 rounded-lg bg-white/[0.07] p-3">
|
||||||
|
<Text className="text-[13px] font-black text-white/70">
|
||||||
|
叙事节奏
|
||||||
|
</Text>
|
||||||
|
<ChipRow
|
||||||
|
compact
|
||||||
|
options={creationOptions.rhythms}
|
||||||
|
value={rhythm}
|
||||||
|
onChange={setRhythm}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="gap-3 rounded-lg bg-white/[0.07] p-3.5">
|
||||||
|
<Text className="text-[15px] font-black text-white">生成流程</Text>
|
||||||
|
<View className="flex-row flex-wrap gap-2.5">
|
||||||
|
{["故事圣经", "分镜脚本", "角色视觉", "首集预览"].map(
|
||||||
|
(step, index) => (
|
||||||
|
<View
|
||||||
|
key={step}
|
||||||
|
className="min-w-[46%] flex-row items-center gap-2"
|
||||||
|
>
|
||||||
|
<Text className="h-[22px] w-[22px] overflow-hidden rounded-full bg-[#b8ff5d] text-center text-xs font-black leading-[22px] text-[#09090b]">
|
||||||
|
{index + 1}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[13px] font-extrabold text-white/80">
|
||||||
|
{step}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<View style={styles.footer}>
|
<View
|
||||||
|
className="absolute bottom-0 left-0 right-0 bg-[#09090b]/95 px-[18px] pt-3"
|
||||||
|
style={{ paddingBottom: BottomTabInset + 14 }}
|
||||||
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
disabled={!ready}
|
disabled={!ready}
|
||||||
style={({ pressed }) => [
|
className={`h-[52px] flex-row items-center justify-center gap-2 rounded-full bg-white ${!ready ? "opacity-40" : ""}`}
|
||||||
styles.generateButton,
|
style={({ pressed }) => ({
|
||||||
!ready && styles.disabledButton,
|
opacity: pressed && ready ? 0.76 : undefined,
|
||||||
pressed && ready && styles.pressed,
|
})}
|
||||||
]}>
|
>
|
||||||
<SymbolView name={{ ios: 'wand.and.stars', android: 'auto_fix_high', web: 'auto_fix_high' }} size={18} tintColor="#050505" />
|
<SymbolView
|
||||||
<Text style={styles.generateText}>{ready ? '生成第一集' : '继续补充设定'}</Text>
|
name={{
|
||||||
|
ios: "wand.and.stars",
|
||||||
|
android: "auto_fix_high",
|
||||||
|
web: "auto_fix_high",
|
||||||
|
}}
|
||||||
|
size={18}
|
||||||
|
tintColor="#050505"
|
||||||
|
/>
|
||||||
|
<Text className="text-base font-black text-[#050505]">
|
||||||
|
{ready ? "生成第一集" : "继续补充设定"}
|
||||||
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
@@ -127,216 +185,26 @@ function ChipRow({
|
|||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.chips, compact && styles.compactChips]}>
|
<View className={`flex-row flex-wrap ${compact ? "gap-2" : "gap-2.5"}`}>
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
const selected = option === value;
|
const selected = option === value;
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={option}
|
key={option}
|
||||||
onPress={() => onChange(option)}
|
onPress={() => onChange(option)}
|
||||||
style={({ pressed }) => [
|
className={`items-center justify-center rounded-full border ${
|
||||||
styles.chip,
|
compact ? "min-h-8 px-2.5" : "min-h-[38px] px-3.5"
|
||||||
compact && styles.compactChip,
|
} ${selected ? "border-white bg-white" : "border-white/10 bg-white/10"}`}
|
||||||
selected && styles.selectedChip,
|
style={({ pressed }) => ({ opacity: pressed ? 0.76 : undefined })}
|
||||||
pressed && styles.pressed,
|
>
|
||||||
]}>
|
<Text
|
||||||
<Text style={[styles.chipText, selected && styles.selectedChipText]}>{option}</Text>
|
className={`text-[13px] font-extrabold ${selected ? "text-[#09090b]" : "text-white"}`}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
safe: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#09090b',
|
|
||||||
},
|
|
||||||
scroll: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
padding: 18,
|
|
||||||
paddingBottom: BottomTabInset + 112,
|
|
||||||
gap: 18,
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
gap: 18,
|
|
||||||
paddingTop: 8,
|
|
||||||
},
|
|
||||||
eyebrow: {
|
|
||||||
color: '#ff4d6d',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '900',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 30,
|
|
||||||
lineHeight: 36,
|
|
||||||
fontWeight: '900',
|
|
||||||
maxWidth: 300,
|
|
||||||
},
|
|
||||||
headerIcon: {
|
|
||||||
width: 46,
|
|
||||||
height: 46,
|
|
||||||
borderRadius: 23,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
|
||||||
},
|
|
||||||
section: {
|
|
||||||
gap: 10,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
color: 'rgba(255,255,255,0.7)',
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
chips: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: 10,
|
|
||||||
},
|
|
||||||
compactChips: {
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
chip: {
|
|
||||||
minHeight: 38,
|
|
||||||
borderRadius: 19,
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.09)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255,255,255,0.08)',
|
|
||||||
},
|
|
||||||
compactChip: {
|
|
||||||
minHeight: 32,
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
},
|
|
||||||
selectedChip: {
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
borderColor: '#fff',
|
|
||||||
},
|
|
||||||
chipText: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '800',
|
|
||||||
},
|
|
||||||
selectedChipText: {
|
|
||||||
color: '#09090b',
|
|
||||||
},
|
|
||||||
fieldBlock: {
|
|
||||||
gap: 10,
|
|
||||||
},
|
|
||||||
fieldHeader: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
counter: {
|
|
||||||
color: 'rgba(255,255,255,0.42)',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '800',
|
|
||||||
},
|
|
||||||
textArea: {
|
|
||||||
minHeight: 158,
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 14,
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 16,
|
|
||||||
lineHeight: 24,
|
|
||||||
fontWeight: '600',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255,255,255,0.1)',
|
|
||||||
},
|
|
||||||
shortArea: {
|
|
||||||
minHeight: 116,
|
|
||||||
},
|
|
||||||
twoColumns: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
optionCard: {
|
|
||||||
flex: 1,
|
|
||||||
gap: 12,
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 12,
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.07)',
|
|
||||||
},
|
|
||||||
previewCard: {
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 14,
|
|
||||||
gap: 12,
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.07)',
|
|
||||||
},
|
|
||||||
previewTitle: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
steps: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: 10,
|
|
||||||
},
|
|
||||||
step: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
minWidth: '46%',
|
|
||||||
},
|
|
||||||
stepIndex: {
|
|
||||||
width: 22,
|
|
||||||
height: 22,
|
|
||||||
borderRadius: 11,
|
|
||||||
overflow: 'hidden',
|
|
||||||
textAlign: 'center',
|
|
||||||
lineHeight: 22,
|
|
||||||
color: '#09090b',
|
|
||||||
backgroundColor: '#b8ff5d',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
stepText: {
|
|
||||||
color: 'rgba(255,255,255,0.82)',
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '800',
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
paddingHorizontal: 18,
|
|
||||||
paddingTop: 12,
|
|
||||||
paddingBottom: BottomTabInset + 14,
|
|
||||||
backgroundColor: 'rgba(9,9,11,0.96)',
|
|
||||||
},
|
|
||||||
generateButton: {
|
|
||||||
height: 52,
|
|
||||||
borderRadius: 26,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 8,
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
},
|
|
||||||
disabledButton: {
|
|
||||||
opacity: 0.42,
|
|
||||||
},
|
|
||||||
generateText: {
|
|
||||||
color: '#050505',
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
pressed: {
|
|
||||||
opacity: 0.76,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
+116
-208
@@ -1,26 +1,28 @@
|
|||||||
import { mobileDramaFeed } from '@infiplot/core';
|
import { mobileDramaFeed } from "@infiplot/core";
|
||||||
import type { MobileDrama } from '@infiplot/types';
|
import type { MobileDrama } from "@infiplot/types";
|
||||||
import { SymbolView } from 'expo-symbols';
|
import { SymbolView } from "expo-symbols";
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from "react";
|
||||||
import { useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
FlatList,
|
FlatList,
|
||||||
Pressable,
|
Pressable,
|
||||||
StyleSheet,
|
|
||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
type NativeScrollEvent,
|
type NativeScrollEvent,
|
||||||
type NativeSyntheticEvent,
|
type NativeSyntheticEvent,
|
||||||
} from 'react-native';
|
} from "react-native";
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import {
|
||||||
|
SafeAreaView,
|
||||||
|
useSafeAreaInsets,
|
||||||
|
} from "react-native-safe-area-context";
|
||||||
|
|
||||||
import { BottomTabInset } from '@/constants/theme';
|
import { BottomTabInset } from "@/constants/theme";
|
||||||
|
|
||||||
type SymbolName = ComponentProps<typeof SymbolView>['name'];
|
type SymbolName = ComponentProps<typeof SymbolView>["name"];
|
||||||
|
|
||||||
export default function FeedScreen() {
|
export default function FeedScreen() {
|
||||||
const { height } = Dimensions.get('window');
|
const { height } = Dimensions.get("window");
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const pageHeight = height;
|
const pageHeight = height;
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
@@ -32,12 +34,16 @@ export default function FeedScreen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
function handleScrollEnd(event: NativeSyntheticEvent<NativeScrollEvent>) {
|
function handleScrollEnd(event: NativeSyntheticEvent<NativeScrollEvent>) {
|
||||||
const nextIndex = Math.round(event.nativeEvent.contentOffset.y / pageHeight);
|
const nextIndex = Math.round(
|
||||||
setActiveIndex(Math.max(0, Math.min(mobileDramaFeed.length - 1, nextIndex)));
|
event.nativeEvent.contentOffset.y / pageHeight,
|
||||||
|
);
|
||||||
|
setActiveIndex(
|
||||||
|
Math.max(0, Math.min(mobileDramaFeed.length - 1, nextIndex)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.root}>
|
<View className="flex-1 bg-black">
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
data={mobileDramaFeed}
|
data={mobileDramaFeed}
|
||||||
@@ -72,45 +78,109 @@ function DramaPage({
|
|||||||
bottomInset: number;
|
bottomInset: number;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.page, { height, backgroundColor: drama.palette[0] }]}>
|
<View
|
||||||
<View style={[styles.posterGlow, { backgroundColor: drama.palette[1] }]} />
|
className="overflow-hidden"
|
||||||
<View style={[styles.posterOrb, { borderColor: drama.palette[2] }]}>
|
style={{ height, backgroundColor: drama.palette[0] }}
|
||||||
<Text style={[styles.posterTitle, { color: drama.palette[2] }]}>{drama.title}</Text>
|
>
|
||||||
<Text style={styles.posterSubtitle}>{drama.episode}</Text>
|
<View
|
||||||
|
className="absolute left-[-120px] top-[14%] h-[520px] w-[520px] rounded-full opacity-50"
|
||||||
|
style={{
|
||||||
|
backgroundColor: drama.palette[1],
|
||||||
|
transform: [{ rotate: "-18deg" }],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
className="absolute top-[18%] w-[76%] self-center rounded-[28px] border bg-white/10 p-6"
|
||||||
|
style={{ aspectRatio: 0.72, borderColor: drama.palette[2] }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="text-center text-[42px] font-extrabold leading-[48px]"
|
||||||
|
style={{ color: drama.palette[2] }}
|
||||||
|
>
|
||||||
|
{drama.title}
|
||||||
|
</Text>
|
||||||
|
<Text className="mt-3.5 text-[13px] font-bold text-white/70">
|
||||||
|
{drama.episode}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<SafeAreaView style={styles.safe}>
|
<SafeAreaView className="flex-1">
|
||||||
<View style={styles.topBar}>
|
<View className="flex-row items-center justify-between px-[18px] pt-1.5">
|
||||||
<Text style={styles.brand}>InfiPlot</Text>
|
<Text className="text-xl font-black text-white">InfiPlot</Text>
|
||||||
<View style={styles.livePill}>
|
<View className="flex-row items-center gap-1.5 rounded-full bg-black/30 px-3 py-[7px]">
|
||||||
<View style={[styles.liveDot, { backgroundColor: active ? '#ff2d55' : '#777' }]} />
|
<View
|
||||||
<Text style={styles.liveText}>刷剧</Text>
|
className="h-[7px] w-[7px] rounded-full"
|
||||||
|
style={{ backgroundColor: active ? "#ff2d55" : "#777" }}
|
||||||
|
/>
|
||||||
|
<Text className="text-xs font-extrabold text-white">刷剧</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={[styles.sideRail, { bottom: bottomInset + BottomTabInset + 86 }]}>
|
<View
|
||||||
<ActionButton name={{ ios: 'heart.fill', android: 'favorite', web: 'favorite' }} label={drama.likes} />
|
className="absolute right-3.5 items-center gap-[18px]"
|
||||||
<ActionButton name={{ ios: 'bubble.right.fill', android: 'chat_bubble', web: 'chat_bubble' }} label={drama.comments} />
|
style={{ bottom: bottomInset + BottomTabInset + 86 }}
|
||||||
<ActionButton name={{ ios: 'square.and.arrow.up', android: 'ios_share', web: 'ios_share' }} label="分享" />
|
>
|
||||||
|
<ActionButton
|
||||||
|
name={{ ios: "heart.fill", android: "favorite", web: "favorite" }}
|
||||||
|
label={drama.likes}
|
||||||
|
/>
|
||||||
|
<ActionButton
|
||||||
|
name={{
|
||||||
|
ios: "bubble.right.fill",
|
||||||
|
android: "chat_bubble",
|
||||||
|
web: "chat_bubble",
|
||||||
|
}}
|
||||||
|
label={drama.comments}
|
||||||
|
/>
|
||||||
|
<ActionButton
|
||||||
|
name={{
|
||||||
|
ios: "square.and.arrow.up",
|
||||||
|
android: "ios_share",
|
||||||
|
web: "ios_share",
|
||||||
|
}}
|
||||||
|
label="分享"
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={[styles.infoPanel, { paddingBottom: bottomInset + BottomTabInset + 20 }]}>
|
<View
|
||||||
<Text style={styles.creator}>@{drama.creator}</Text>
|
className="mt-auto gap-2.5 pl-[18px] pr-[82px]"
|
||||||
<Text style={styles.title}>{drama.title}</Text>
|
style={{ paddingBottom: bottomInset + BottomTabInset + 20 }}
|
||||||
<Text style={styles.hook}>{drama.hook}</Text>
|
>
|
||||||
<View style={styles.tags}>
|
<Text className="text-[15px] font-black text-white">
|
||||||
|
@{drama.creator}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[30px] font-black leading-9 text-white">
|
||||||
|
{drama.title}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[15px] font-semibold leading-[22px] text-white/90">
|
||||||
|
{drama.hook}
|
||||||
|
</Text>
|
||||||
|
<View className="flex-row flex-wrap gap-2">
|
||||||
{drama.tags.map((tag) => (
|
{drama.tags.map((tag) => (
|
||||||
<Text key={tag} style={styles.tag}>
|
<Text key={tag} className="text-[13px] font-extrabold text-white">
|
||||||
#{tag}
|
#{tag}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.playRow}>
|
<View className="mt-1 flex-row items-center gap-3">
|
||||||
<Pressable style={({ pressed }) => [styles.primaryButton, pressed && styles.pressed]}>
|
<Pressable
|
||||||
<SymbolView name={{ ios: 'play.fill', android: 'play_arrow', web: 'play_arrow' }} size={16} tintColor="#101010" />
|
className="h-[42px] flex-row items-center justify-center gap-1.5 rounded-full bg-white px-[18px]"
|
||||||
<Text style={styles.primaryButtonText}>继续看</Text>
|
style={({ pressed }) => ({ opacity: pressed ? 0.75 : undefined })}
|
||||||
|
>
|
||||||
|
<SymbolView
|
||||||
|
name={{
|
||||||
|
ios: "play.fill",
|
||||||
|
android: "play_arrow",
|
||||||
|
web: "play_arrow",
|
||||||
|
}}
|
||||||
|
size={16}
|
||||||
|
tintColor="#101010"
|
||||||
|
/>
|
||||||
|
<Text className="text-sm font-black text-[#101010]">继续看</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Text style={styles.episode}>{drama.episode}</Text>
|
<Text className="shrink text-xs font-bold text-white/80">
|
||||||
|
{drama.episode}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
@@ -120,176 +190,14 @@ function DramaPage({
|
|||||||
|
|
||||||
function ActionButton({ name, label }: { name: SymbolName; label: string }) {
|
function ActionButton({ name, label }: { name: SymbolName; label: string }) {
|
||||||
return (
|
return (
|
||||||
<Pressable style={({ pressed }) => [styles.actionButton, pressed && styles.pressed]}>
|
<Pressable
|
||||||
<View style={styles.actionIcon}>
|
className="items-center gap-1.5"
|
||||||
|
style={({ pressed }) => ({ opacity: pressed ? 0.75 : undefined })}
|
||||||
|
>
|
||||||
|
<View className="h-12 w-12 items-center justify-center rounded-full bg-white/15">
|
||||||
<SymbolView name={name} size={24} tintColor="#fff" />
|
<SymbolView name={name} size={24} tintColor="#fff" />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.actionLabel}>{label}</Text>
|
<Text className="text-[11px] font-extrabold text-white">{label}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
root: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#000',
|
|
||||||
},
|
|
||||||
page: {
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
posterGlow: {
|
|
||||||
position: 'absolute',
|
|
||||||
width: 520,
|
|
||||||
height: 520,
|
|
||||||
borderRadius: 260,
|
|
||||||
top: '14%',
|
|
||||||
left: -120,
|
|
||||||
opacity: 0.5,
|
|
||||||
transform: [{ rotate: '-18deg' }],
|
|
||||||
},
|
|
||||||
posterOrb: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: '18%',
|
|
||||||
alignSelf: 'center',
|
|
||||||
width: '76%',
|
|
||||||
aspectRatio: 0.72,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 28,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 24,
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
||||||
},
|
|
||||||
posterTitle: {
|
|
||||||
fontSize: 42,
|
|
||||||
lineHeight: 48,
|
|
||||||
fontWeight: '800',
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
posterSubtitle: {
|
|
||||||
color: 'rgba(255,255,255,0.72)',
|
|
||||||
marginTop: 14,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '700',
|
|
||||||
},
|
|
||||||
safe: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
topBar: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
paddingHorizontal: 18,
|
|
||||||
paddingTop: 6,
|
|
||||||
},
|
|
||||||
brand: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
livePill: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 6,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 7,
|
|
||||||
borderRadius: 99,
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.32)',
|
|
||||||
},
|
|
||||||
liveDot: {
|
|
||||||
width: 7,
|
|
||||||
height: 7,
|
|
||||||
borderRadius: 7,
|
|
||||||
},
|
|
||||||
liveText: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '800',
|
|
||||||
},
|
|
||||||
sideRail: {
|
|
||||||
position: 'absolute',
|
|
||||||
right: 14,
|
|
||||||
gap: 18,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
actionButton: {
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 6,
|
|
||||||
},
|
|
||||||
actionIcon: {
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
borderRadius: 24,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.16)',
|
|
||||||
},
|
|
||||||
actionLabel: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: '800',
|
|
||||||
},
|
|
||||||
infoPanel: {
|
|
||||||
marginTop: 'auto',
|
|
||||||
paddingLeft: 18,
|
|
||||||
paddingRight: 82,
|
|
||||||
gap: 10,
|
|
||||||
},
|
|
||||||
creator: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 30,
|
|
||||||
lineHeight: 36,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
hook: {
|
|
||||||
color: 'rgba(255,255,255,0.88)',
|
|
||||||
fontSize: 15,
|
|
||||||
lineHeight: 22,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
tags: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
tag: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '800',
|
|
||||||
},
|
|
||||||
playRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
primaryButton: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 6,
|
|
||||||
paddingHorizontal: 18,
|
|
||||||
height: 42,
|
|
||||||
borderRadius: 21,
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
},
|
|
||||||
primaryButtonText: {
|
|
||||||
color: '#101010',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
episode: {
|
|
||||||
color: 'rgba(255,255,255,0.78)',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '700',
|
|
||||||
flexShrink: 1,
|
|
||||||
},
|
|
||||||
pressed: {
|
|
||||||
opacity: 0.75,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
+313
-524
@@ -1,27 +1,26 @@
|
|||||||
import type { ModelConfig, ModelConfigMap, ModelRole } from '@infiplot/types';
|
import type { ModelConfig, ModelConfigMap, ModelRole } from "@infiplot/types";
|
||||||
import { SymbolView } from 'expo-symbols';
|
import { SymbolView } from "expo-symbols";
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from "react";
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Pressable,
|
Pressable,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
|
||||||
Switch,
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from "react-native";
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
import { BottomTabInset } from '@/constants/theme';
|
import { BottomTabInset } from "@/constants/theme";
|
||||||
import {
|
import {
|
||||||
clearModelConfig,
|
clearModelConfig,
|
||||||
defaultModelConfig,
|
defaultModelConfig,
|
||||||
readModelConfig,
|
readModelConfig,
|
||||||
writeModelConfig,
|
writeModelConfig,
|
||||||
} from '@/lib/model-config';
|
} from "@/lib/model-config";
|
||||||
|
|
||||||
type SymbolName = ComponentProps<typeof SymbolView>['name'];
|
type SymbolName = ComponentProps<typeof SymbolView>["name"];
|
||||||
|
|
||||||
const modelGroups: {
|
const modelGroups: {
|
||||||
id: ModelRole;
|
id: ModelRole;
|
||||||
@@ -29,28 +28,46 @@ const modelGroups: {
|
|||||||
hint: string;
|
hint: string;
|
||||||
accent: string;
|
accent: string;
|
||||||
}[] = [
|
}[] = [
|
||||||
{ id: 'text', title: '文本模型', hint: '剧情、分镜、选择分支', accent: '#ff4d6d' },
|
{
|
||||||
{ id: 'image', title: '图像模型', hint: '角色、场景、封面生成', accent: '#b8ff5d' },
|
id: "text",
|
||||||
{ id: 'vision', title: '视觉理解', hint: '风格图解析、画面标注', accent: '#78d7ff' },
|
title: "文本模型",
|
||||||
{ id: 'tts', title: '语音模型', hint: '对白配音、旁白', accent: '#ffd166' },
|
hint: "剧情、分镜、选择分支",
|
||||||
|
accent: "#ff4d6d",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "image",
|
||||||
|
title: "图像模型",
|
||||||
|
hint: "角色、场景、封面生成",
|
||||||
|
accent: "#b8ff5d",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "vision",
|
||||||
|
title: "视觉理解",
|
||||||
|
hint: "风格图解析、画面标注",
|
||||||
|
accent: "#78d7ff",
|
||||||
|
},
|
||||||
|
{ id: "tts", title: "语音模型", hint: "对白配音、旁白", accent: "#ffd166" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const roleLabels: Record<ModelRole, string> = {
|
const roleLabels: Record<ModelRole, string> = {
|
||||||
text: '文本',
|
text: "文本",
|
||||||
image: '图像',
|
image: "图像",
|
||||||
vision: '视觉',
|
vision: "视觉",
|
||||||
tts: '语音',
|
tts: "语音",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const [syncEnabled, setSyncEnabled] = useState(true);
|
const [syncEnabled, setSyncEnabled] = useState(true);
|
||||||
const [showKeys, setShowKeys] = useState(false);
|
const [showKeys, setShowKeys] = useState(false);
|
||||||
const [signedIn, setSignedIn] = useState(false);
|
const [signedIn, setSignedIn] = useState(false);
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState("");
|
||||||
const [selectedRole, setSelectedRole] = useState<ModelRole>('text');
|
const [selectedRole, setSelectedRole] = useState<ModelRole>("text");
|
||||||
const [configs, setConfigs] = useState<ModelConfigMap>(defaultModelConfig);
|
const [configs, setConfigs] = useState<ModelConfigMap>(defaultModelConfig);
|
||||||
const [savedConfigs, setSavedConfigs] = useState<ModelConfigMap>(defaultModelConfig);
|
const [savedConfigs, setSavedConfigs] =
|
||||||
const [status, setStatus] = useState<'loading' | 'saved' | 'dirty'>('loading');
|
useState<ModelConfigMap>(defaultModelConfig);
|
||||||
|
const [status, setStatus] = useState<"loading" | "saved" | "dirty">(
|
||||||
|
"loading",
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
@@ -59,7 +76,7 @@ export default function ProfileScreen() {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setConfigs(stored);
|
setConfigs(stored);
|
||||||
setSavedConfigs(stored);
|
setSavedConfigs(stored);
|
||||||
setStatus('saved');
|
setStatus("saved");
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -67,11 +84,14 @@ export default function ProfileScreen() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const selectedGroup = modelGroups.find((group) => group.id === selectedRole) ?? modelGroups[0];
|
const selectedGroup =
|
||||||
|
modelGroups.find((group) => group.id === selectedRole) ?? modelGroups[0];
|
||||||
const selectedConfig = configs[selectedRole];
|
const selectedConfig = configs[selectedRole];
|
||||||
|
|
||||||
const configuredCount = useMemo(
|
const configuredCount = useMemo(
|
||||||
() => Object.values(configs).filter((config) => config.apiKey.trim().length > 0).length,
|
() =>
|
||||||
|
Object.values(configs).filter((config) => config.apiKey.trim().length > 0)
|
||||||
|
.length,
|
||||||
[configs],
|
[configs],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -88,28 +108,28 @@ export default function ProfileScreen() {
|
|||||||
[field]: value,
|
[field]: value,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
setStatus('dirty');
|
setStatus("dirty");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
const saved = await writeModelConfig(configs);
|
const saved = await writeModelConfig(configs);
|
||||||
setConfigs(saved);
|
setConfigs(saved);
|
||||||
setSavedConfigs(saved);
|
setSavedConfigs(saved);
|
||||||
setStatus('saved');
|
setStatus("saved");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleReset() {
|
async function handleReset() {
|
||||||
const reset = await clearModelConfig();
|
const reset = await clearModelConfig();
|
||||||
setConfigs(reset);
|
setConfigs(reset);
|
||||||
setSavedConfigs(reset);
|
setSavedConfigs(reset);
|
||||||
setSelectedRole('text');
|
setSelectedRole("text");
|
||||||
setStatus('saved');
|
setStatus("saved");
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAuthAction() {
|
function handleAuthAction() {
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
setSignedIn(false);
|
setSignedIn(false);
|
||||||
setEmail('');
|
setEmail("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,179 +139,263 @@ export default function ProfileScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe}>
|
<SafeAreaView className="flex-1 bg-[#09090b]">
|
||||||
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
<ScrollView
|
||||||
<View style={styles.profileCard}>
|
contentContainerStyle={{ paddingBottom: BottomTabInset + 34 }}
|
||||||
<View style={styles.avatar}>
|
keyboardShouldPersistTaps="handled"
|
||||||
<Text style={styles.avatarText}>IP</Text>
|
>
|
||||||
</View>
|
<View className="gap-[18px] p-[18px]">
|
||||||
<View style={styles.profileCopy}>
|
<View className="flex-row items-center gap-3 rounded-lg bg-white/10 p-3.5">
|
||||||
<Text style={styles.name}>{signedIn ? 'InfiPlot 创作者' : '未登录创作者'}</Text>
|
<View className="h-[54px] w-[54px] items-center justify-center rounded-full bg-[#ff4d6d]">
|
||||||
<Text style={styles.subline}>
|
<Text className="text-lg font-black text-white">IP</Text>
|
||||||
{signedIn ? email.trim() : '登录后同步我的剧情、草稿和模型配置。'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<Pressable
|
|
||||||
onPress={handleAuthAction}
|
|
||||||
style={({ pressed }) => [styles.loginButton, pressed && styles.pressed]}>
|
|
||||||
<Text style={styles.loginText}>{signedIn ? '退出' : '登录'}</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{!signedIn && (
|
|
||||||
<View style={styles.loginPanel}>
|
|
||||||
<TextInput
|
|
||||||
value={email}
|
|
||||||
onChangeText={setEmail}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect={false}
|
|
||||||
keyboardType="email-address"
|
|
||||||
placeholder="creator@infiplot.app"
|
|
||||||
placeholderTextColor="rgba(255,255,255,0.35)"
|
|
||||||
style={styles.loginInput}
|
|
||||||
/>
|
|
||||||
<Pressable
|
|
||||||
onPress={handleAuthAction}
|
|
||||||
disabled={email.trim().length <= 3}
|
|
||||||
style={({ pressed }) => [
|
|
||||||
styles.loginSubmit,
|
|
||||||
email.trim().length <= 3 && styles.disabledButton,
|
|
||||||
pressed && email.trim().length > 3 && styles.pressed,
|
|
||||||
]}>
|
|
||||||
<Text style={styles.loginSubmitText}>继续</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View style={styles.stats}>
|
|
||||||
<Stat value="12" label="草稿" />
|
|
||||||
<Stat value="4" label="已发布" />
|
|
||||||
<Stat value={`${configuredCount}/4`} label="API" />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.sectionHeader}>
|
|
||||||
<View>
|
|
||||||
<Text style={styles.sectionTitle}>模型 API</Text>
|
|
||||||
<Text style={styles.sectionHint}>对应 web 端文字、图片、视觉和 TTS 配置。</Text>
|
|
||||||
</View>
|
|
||||||
<Pressable
|
|
||||||
onPress={() => setShowKeys((value) => !value)}
|
|
||||||
style={({ pressed }) => [styles.iconButton, pressed && styles.pressed]}>
|
|
||||||
<SymbolView
|
|
||||||
name={{
|
|
||||||
ios: showKeys ? 'eye.slash.fill' : 'eye.fill',
|
|
||||||
android: showKeys ? 'visibility_off' : 'visibility',
|
|
||||||
web: showKeys ? 'visibility_off' : 'visibility',
|
|
||||||
}}
|
|
||||||
size={18}
|
|
||||||
tintColor="#fff"
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.roleTabs}>
|
|
||||||
{modelGroups.map((group) => {
|
|
||||||
const selected = group.id === selectedRole;
|
|
||||||
const configured = configs[group.id].apiKey.trim().length > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
key={group.id}
|
|
||||||
onPress={() => setSelectedRole(group.id)}
|
|
||||||
style={({ pressed }) => [
|
|
||||||
styles.roleTab,
|
|
||||||
selected && styles.selectedRoleTab,
|
|
||||||
pressed && styles.pressed,
|
|
||||||
]}>
|
|
||||||
<View style={[styles.roleDot, { backgroundColor: configured ? group.accent : '#4a4a52' }]} />
|
|
||||||
<Text style={[styles.roleText, selected && styles.selectedRoleText]}>
|
|
||||||
{roleLabels[group.id]}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.modelEditor}>
|
|
||||||
<View style={styles.modelTop}>
|
|
||||||
<View>
|
|
||||||
<Text style={styles.modelTitle}>{selectedGroup.title}</Text>
|
|
||||||
<Text style={styles.modelHint}>{selectedGroup.hint}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.statusPill, dirty && styles.dirtyPill]}>
|
<View className="flex-1 gap-[3px]">
|
||||||
<Text style={[styles.statusText, dirty && styles.dirtyText]}>
|
<Text className="text-lg font-black text-white">
|
||||||
{status === 'loading' ? '读取中' : dirty ? '未保存' : '已保存'}
|
{signedIn ? "InfiPlot 创作者" : "未登录创作者"}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs font-bold leading-[17px] text-white/60">
|
||||||
|
{signedIn
|
||||||
|
? email.trim()
|
||||||
|
: "登录后同步我的剧情、草稿和模型配置。"}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<Pressable
|
||||||
|
onPress={handleAuthAction}
|
||||||
|
className="h-9 justify-center rounded-full bg-white px-4"
|
||||||
|
style={({ pressed }) => ({ opacity: pressed ? 0.76 : undefined })}
|
||||||
|
>
|
||||||
|
<Text className="text-[13px] font-black text-[#09090b]">
|
||||||
|
{signedIn ? "退出" : "登录"}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<InputLine
|
{!signedIn && (
|
||||||
label="Provider"
|
<View className="flex-row gap-2.5 rounded-lg bg-white/[0.06] p-3">
|
||||||
value={selectedConfig.provider}
|
<TextInput
|
||||||
onChangeText={(value) => updateConfig('provider', value)}
|
value={email}
|
||||||
placeholder="openai / claude / gemini / stepfun"
|
onChangeText={setEmail}
|
||||||
/>
|
autoCapitalize="none"
|
||||||
<InputLine
|
autoCorrect={false}
|
||||||
label="Base URL"
|
keyboardType="email-address"
|
||||||
value={selectedConfig.baseUrl}
|
placeholder="creator@infiplot.app"
|
||||||
onChangeText={(value) => updateConfig('baseUrl', value)}
|
placeholderTextColor="rgba(255,255,255,0.35)"
|
||||||
placeholder="https://api.example.com/v1"
|
className="min-h-[42px] flex-1 rounded-lg border border-white/10 bg-black/20 px-3 text-[13px] font-bold text-white"
|
||||||
/>
|
/>
|
||||||
<InputLine
|
<Pressable
|
||||||
label="API Key"
|
onPress={handleAuthAction}
|
||||||
value={selectedConfig.apiKey}
|
disabled={email.trim().length <= 3}
|
||||||
onChangeText={(value) => updateConfig('apiKey', value)}
|
className={`h-[42px] min-w-[70px] items-center justify-center rounded-full bg-white ${
|
||||||
secureTextEntry={!showKeys}
|
email.trim().length <= 3 ? "opacity-40" : ""
|
||||||
placeholder="sk-..."
|
}`}
|
||||||
/>
|
style={({ pressed }) => ({
|
||||||
<InputLine
|
opacity:
|
||||||
label="Model"
|
pressed && email.trim().length > 3 ? 0.76 : undefined,
|
||||||
value={selectedConfig.model}
|
})}
|
||||||
onChangeText={(value) => updateConfig('model', value)}
|
>
|
||||||
placeholder="model name"
|
<Text className="text-[13px] font-black text-[#09090b]">
|
||||||
/>
|
继续
|
||||||
|
</Text>
|
||||||
<View style={styles.actionRow}>
|
</Pressable>
|
||||||
<Pressable
|
</View>
|
||||||
onPress={handleReset}
|
)}
|
||||||
style={({ pressed }) => [styles.secondaryButton, pressed && styles.pressed]}>
|
|
||||||
<Text style={styles.secondaryText}>重置</Text>
|
<View className="flex-row rounded-lg bg-white/[0.06] py-3.5">
|
||||||
</Pressable>
|
<Stat value="12" label="草稿" />
|
||||||
<Pressable
|
<Stat value="4" label="已发布" />
|
||||||
onPress={handleSave}
|
<Stat value={`${configuredCount}/4`} label="API" />
|
||||||
disabled={!dirty}
|
</View>
|
||||||
style={({ pressed }) => [
|
|
||||||
styles.saveButton,
|
<View className="flex-row items-center justify-between gap-4">
|
||||||
!dirty && styles.disabledButton,
|
<View>
|
||||||
pressed && dirty && styles.pressed,
|
<Text className="text-[23px] font-black text-white">
|
||||||
]}>
|
模型 API
|
||||||
<SymbolView
|
</Text>
|
||||||
name={{ ios: 'checkmark.circle.fill', android: 'check_circle', web: 'check_circle' }}
|
<Text className="mt-1 text-xs font-bold leading-[18px] text-white/60">
|
||||||
size={18}
|
对应 web 端文字、图片、视觉和 TTS 配置。
|
||||||
tintColor="#09090b"
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setShowKeys((value) => !value)}
|
||||||
|
className="h-[42px] w-[42px] items-center justify-center rounded-full bg-white/10"
|
||||||
|
style={({ pressed }) => ({ opacity: pressed ? 0.76 : undefined })}
|
||||||
|
>
|
||||||
|
<SymbolView
|
||||||
|
name={{
|
||||||
|
ios: showKeys ? "eye.slash.fill" : "eye.fill",
|
||||||
|
android: showKeys ? "visibility_off" : "visibility",
|
||||||
|
web: showKeys ? "visibility_off" : "visibility",
|
||||||
|
}}
|
||||||
|
size={18}
|
||||||
|
tintColor="#fff"
|
||||||
/>
|
/>
|
||||||
<Text style={styles.saveText}>保存配置</Text>
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.settingRow}>
|
<View className="flex-row gap-2">
|
||||||
<View style={styles.settingTextBlock}>
|
{modelGroups.map((group) => {
|
||||||
<Text style={styles.settingTitle}>云端同步</Text>
|
const selected = group.id === selectedRole;
|
||||||
<Text style={styles.settingHint}>登录后自动同步我的剧情和配置。</Text>
|
const configured = configs[group.id].apiKey.trim().length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={group.id}
|
||||||
|
onPress={() => setSelectedRole(group.id)}
|
||||||
|
className={`min-h-[42px] flex-1 flex-row items-center justify-center gap-1.5 rounded-lg border ${
|
||||||
|
selected
|
||||||
|
? "border-white/20 bg-white/[0.14]"
|
||||||
|
: "border-white/10 bg-white/[0.07]"
|
||||||
|
}`}
|
||||||
|
style={({ pressed }) => ({
|
||||||
|
opacity: pressed ? 0.76 : undefined,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
className="h-[7px] w-[7px] rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: configured ? group.accent : "#4a4a52",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
className={`text-xs font-black ${selected ? "text-white" : "text-white/60"}`}
|
||||||
|
>
|
||||||
|
{roleLabels[group.id]}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
|
||||||
value={syncEnabled}
|
|
||||||
onValueChange={setSyncEnabled}
|
|
||||||
trackColor={{ false: '#303036', true: '#ff4d6d' }}
|
|
||||||
thumbColor="#fff"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.menuList}>
|
<View className="gap-3 rounded-lg border border-white/10 bg-white/[0.075] p-3.5">
|
||||||
<MenuItem name={{ ios: 'tray.and.arrow.down.fill', android: 'download', web: 'download' }} label="导入 .infiplot 剧情" />
|
<View className="flex-row justify-between gap-3">
|
||||||
<MenuItem name={{ ios: 'lock.shield.fill', android: 'shield', web: 'shield' }} label="隐私与本地密钥" />
|
<View>
|
||||||
<MenuItem name={{ ios: 'questionmark.circle.fill', android: 'help', web: 'help' }} label="帮助与反馈" />
|
<Text className="text-[17px] font-black text-white">
|
||||||
|
{selectedGroup.title}
|
||||||
|
</Text>
|
||||||
|
<Text className="mt-[3px] text-xs font-bold text-white/50">
|
||||||
|
{selectedGroup.hint}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
className={`h-[26px] justify-center rounded-full px-2.5 ${dirty ? "bg-[#ffd166]/15" : "bg-[#b8ff5d]/15"}`}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className={`text-[11px] font-black ${dirty ? "text-[#ffd166]" : "text-[#b8ff5d]"}`}
|
||||||
|
>
|
||||||
|
{status === "loading"
|
||||||
|
? "读取中"
|
||||||
|
: dirty
|
||||||
|
? "未保存"
|
||||||
|
: "已保存"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<InputLine
|
||||||
|
label="Provider"
|
||||||
|
value={selectedConfig.provider}
|
||||||
|
onChangeText={(value) => updateConfig("provider", value)}
|
||||||
|
placeholder="openai / claude / gemini / stepfun"
|
||||||
|
/>
|
||||||
|
<InputLine
|
||||||
|
label="Base URL"
|
||||||
|
value={selectedConfig.baseUrl}
|
||||||
|
onChangeText={(value) => updateConfig("baseUrl", value)}
|
||||||
|
placeholder="https://api.example.com/v1"
|
||||||
|
/>
|
||||||
|
<InputLine
|
||||||
|
label="API Key"
|
||||||
|
value={selectedConfig.apiKey}
|
||||||
|
onChangeText={(value) => updateConfig("apiKey", value)}
|
||||||
|
secureTextEntry={!showKeys}
|
||||||
|
placeholder="sk-..."
|
||||||
|
/>
|
||||||
|
<InputLine
|
||||||
|
label="Model"
|
||||||
|
value={selectedConfig.model}
|
||||||
|
onChangeText={(value) => updateConfig("model", value)}
|
||||||
|
placeholder="model name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View className="mt-0.5 flex-row gap-2.5">
|
||||||
|
<Pressable
|
||||||
|
onPress={handleReset}
|
||||||
|
className="h-11 justify-center rounded-full bg-white/10 px-[18px]"
|
||||||
|
style={({ pressed }) => ({
|
||||||
|
opacity: pressed ? 0.76 : undefined,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Text className="text-sm font-black text-white">重置</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={handleSave}
|
||||||
|
disabled={!dirty}
|
||||||
|
className={`h-11 flex-1 flex-row items-center justify-center gap-[7px] rounded-full bg-white ${
|
||||||
|
!dirty ? "opacity-40" : ""
|
||||||
|
}`}
|
||||||
|
style={({ pressed }) => ({
|
||||||
|
opacity: pressed && dirty ? 0.76 : undefined,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<SymbolView
|
||||||
|
name={{
|
||||||
|
ios: "checkmark.circle.fill",
|
||||||
|
android: "check_circle",
|
||||||
|
web: "check_circle",
|
||||||
|
}}
|
||||||
|
size={18}
|
||||||
|
tintColor="#09090b"
|
||||||
|
/>
|
||||||
|
<Text className="text-sm font-black text-[#09090b]">
|
||||||
|
保存配置
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="flex-row items-center justify-between gap-[18px] rounded-lg bg-white/[0.07] p-3.5">
|
||||||
|
<View className="flex-1 gap-[3px]">
|
||||||
|
<Text className="text-[15px] font-black text-white">
|
||||||
|
云端同步
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs font-bold text-white/50">
|
||||||
|
登录后自动同步我的剧情和配置。
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={syncEnabled}
|
||||||
|
onValueChange={setSyncEnabled}
|
||||||
|
trackColor={{ false: "#303036", true: "#ff4d6d" }}
|
||||||
|
thumbColor="#fff"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="overflow-hidden rounded-lg bg-white/[0.07]">
|
||||||
|
<MenuItem
|
||||||
|
name={{
|
||||||
|
ios: "tray.and.arrow.down.fill",
|
||||||
|
android: "download",
|
||||||
|
web: "download",
|
||||||
|
}}
|
||||||
|
label="导入 .infiplot 剧情"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name={{
|
||||||
|
ios: "lock.shield.fill",
|
||||||
|
android: "shield",
|
||||||
|
web: "shield",
|
||||||
|
}}
|
||||||
|
label="隐私与本地密钥"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name={{
|
||||||
|
ios: "questionmark.circle.fill",
|
||||||
|
android: "help",
|
||||||
|
web: "help",
|
||||||
|
}}
|
||||||
|
label="帮助与反馈"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
@@ -300,9 +404,9 @@ export default function ProfileScreen() {
|
|||||||
|
|
||||||
function Stat({ value, label }: { value: string; label: string }) {
|
function Stat({ value, label }: { value: string; label: string }) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.stat}>
|
<View className="flex-1 items-center gap-1">
|
||||||
<Text style={styles.statValue}>{value}</Text>
|
<Text className="text-xl font-black text-white">{value}</Text>
|
||||||
<Text style={styles.statLabel}>{label}</Text>
|
<Text className="text-xs font-extrabold text-white/50">{label}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -321,15 +425,15 @@ function InputLine({
|
|||||||
placeholder: string;
|
placeholder: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.inputLine}>
|
<View className="gap-1.5">
|
||||||
<Text style={styles.inputLabel}>{label}</Text>
|
<Text className="text-[11px] font-black text-white/50">{label}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={value}
|
value={value}
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
secureTextEntry={secureTextEntry}
|
secureTextEntry={secureTextEntry}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
style={styles.input}
|
className="min-h-[42px] rounded-lg border border-white/10 bg-black/20 px-3 text-[13px] font-bold text-white"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
placeholderTextColor="rgba(255,255,255,0.35)"
|
placeholderTextColor="rgba(255,255,255,0.35)"
|
||||||
/>
|
/>
|
||||||
@@ -339,338 +443,23 @@ function InputLine({
|
|||||||
|
|
||||||
function MenuItem({ name, label }: { name: SymbolName; label: string }) {
|
function MenuItem({ name, label }: { name: SymbolName; label: string }) {
|
||||||
return (
|
return (
|
||||||
<Pressable style={({ pressed }) => [styles.menuItem, pressed && styles.pressed]}>
|
<Pressable
|
||||||
<View style={styles.menuIcon}>
|
className="min-h-14 flex-row items-center gap-3 border-b border-white/10 px-3.5"
|
||||||
|
style={({ pressed }) => ({ opacity: pressed ? 0.76 : undefined })}
|
||||||
|
>
|
||||||
|
<View className="h-[34px] w-[34px] items-center justify-center rounded-full bg-white/10">
|
||||||
<SymbolView name={name} size={18} tintColor="#fff" />
|
<SymbolView name={name} size={18} tintColor="#fff" />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.menuLabel}>{label}</Text>
|
<Text className="flex-1 text-sm font-extrabold text-white">{label}</Text>
|
||||||
<SymbolView
|
<SymbolView
|
||||||
name={{ ios: 'chevron.right', android: 'chevron_right', web: 'chevron_right' }}
|
name={{
|
||||||
|
ios: "chevron.right",
|
||||||
|
android: "chevron_right",
|
||||||
|
web: "chevron_right",
|
||||||
|
}}
|
||||||
size={14}
|
size={14}
|
||||||
tintColor="rgba(255,255,255,0.4)"
|
tintColor="rgba(255,255,255,0.4)"
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
safe: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#09090b',
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
padding: 18,
|
|
||||||
paddingBottom: BottomTabInset + 34,
|
|
||||||
gap: 18,
|
|
||||||
},
|
|
||||||
profileCard: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 14,
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
||||||
},
|
|
||||||
avatar: {
|
|
||||||
width: 54,
|
|
||||||
height: 54,
|
|
||||||
borderRadius: 27,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: '#ff4d6d',
|
|
||||||
},
|
|
||||||
avatarText: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
profileCopy: {
|
|
||||||
flex: 1,
|
|
||||||
gap: 3,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
subline: {
|
|
||||||
color: 'rgba(255,255,255,0.58)',
|
|
||||||
fontSize: 12,
|
|
||||||
lineHeight: 17,
|
|
||||||
fontWeight: '700',
|
|
||||||
},
|
|
||||||
loginButton: {
|
|
||||||
height: 36,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
borderRadius: 18,
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
},
|
|
||||||
loginText: {
|
|
||||||
color: '#09090b',
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
loginPanel: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 10,
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 12,
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
||||||
},
|
|
||||||
loginInput: {
|
|
||||||
flex: 1,
|
|
||||||
minHeight: 42,
|
|
||||||
borderRadius: 8,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '700',
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.22)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255,255,255,0.08)',
|
|
||||||
},
|
|
||||||
loginSubmit: {
|
|
||||||
minWidth: 70,
|
|
||||||
height: 42,
|
|
||||||
borderRadius: 21,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
},
|
|
||||||
loginSubmitText: {
|
|
||||||
color: '#09090b',
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
stats: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
borderRadius: 8,
|
|
||||||
paddingVertical: 14,
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
||||||
},
|
|
||||||
stat: {
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 4,
|
|
||||||
},
|
|
||||||
statValue: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
statLabel: {
|
|
||||||
color: 'rgba(255,255,255,0.54)',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '800',
|
|
||||||
},
|
|
||||||
sectionHeader: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
gap: 16,
|
|
||||||
},
|
|
||||||
sectionTitle: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 23,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
sectionHint: {
|
|
||||||
color: 'rgba(255,255,255,0.56)',
|
|
||||||
fontSize: 12,
|
|
||||||
lineHeight: 18,
|
|
||||||
fontWeight: '700',
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
iconButton: {
|
|
||||||
width: 42,
|
|
||||||
height: 42,
|
|
||||||
borderRadius: 21,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
||||||
},
|
|
||||||
roleTabs: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
roleTab: {
|
|
||||||
flex: 1,
|
|
||||||
minHeight: 42,
|
|
||||||
borderRadius: 8,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 6,
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.07)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255,255,255,0.08)',
|
|
||||||
},
|
|
||||||
selectedRoleTab: {
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.14)',
|
|
||||||
borderColor: 'rgba(255,255,255,0.2)',
|
|
||||||
},
|
|
||||||
roleDot: {
|
|
||||||
width: 7,
|
|
||||||
height: 7,
|
|
||||||
borderRadius: 7,
|
|
||||||
},
|
|
||||||
roleText: {
|
|
||||||
color: 'rgba(255,255,255,0.58)',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
selectedRoleText: {
|
|
||||||
color: '#fff',
|
|
||||||
},
|
|
||||||
modelEditor: {
|
|
||||||
gap: 12,
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 14,
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.075)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255,255,255,0.08)',
|
|
||||||
},
|
|
||||||
modelTop: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
modelTitle: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 17,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
modelHint: {
|
|
||||||
color: 'rgba(255,255,255,0.52)',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '700',
|
|
||||||
marginTop: 3,
|
|
||||||
},
|
|
||||||
statusPill: {
|
|
||||||
height: 26,
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderRadius: 13,
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
backgroundColor: 'rgba(184,255,93,0.16)',
|
|
||||||
},
|
|
||||||
dirtyPill: {
|
|
||||||
backgroundColor: 'rgba(255,209,102,0.16)',
|
|
||||||
},
|
|
||||||
statusText: {
|
|
||||||
color: '#b8ff5d',
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
dirtyText: {
|
|
||||||
color: '#ffd166',
|
|
||||||
},
|
|
||||||
inputLine: {
|
|
||||||
gap: 6,
|
|
||||||
},
|
|
||||||
inputLabel: {
|
|
||||||
color: 'rgba(255,255,255,0.5)',
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
minHeight: 42,
|
|
||||||
borderRadius: 8,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '700',
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.22)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255,255,255,0.08)',
|
|
||||||
},
|
|
||||||
actionRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 10,
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
|
||||||
secondaryButton: {
|
|
||||||
height: 44,
|
|
||||||
borderRadius: 22,
|
|
||||||
paddingHorizontal: 18,
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
||||||
},
|
|
||||||
secondaryText: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
saveButton: {
|
|
||||||
flex: 1,
|
|
||||||
height: 44,
|
|
||||||
borderRadius: 22,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 7,
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
},
|
|
||||||
disabledButton: {
|
|
||||||
opacity: 0.42,
|
|
||||||
},
|
|
||||||
saveText: {
|
|
||||||
color: '#09090b',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
settingRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
gap: 18,
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 14,
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.07)',
|
|
||||||
},
|
|
||||||
settingTextBlock: {
|
|
||||||
flex: 1,
|
|
||||||
gap: 3,
|
|
||||||
},
|
|
||||||
settingTitle: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: '900',
|
|
||||||
},
|
|
||||||
settingHint: {
|
|
||||||
color: 'rgba(255,255,255,0.52)',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '700',
|
|
||||||
},
|
|
||||||
menuList: {
|
|
||||||
borderRadius: 8,
|
|
||||||
overflow: 'hidden',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.07)',
|
|
||||||
},
|
|
||||||
menuItem: {
|
|
||||||
minHeight: 56,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
||||||
borderBottomColor: 'rgba(255,255,255,0.08)',
|
|
||||||
},
|
|
||||||
menuIcon: {
|
|
||||||
width: 34,
|
|
||||||
height: 34,
|
|
||||||
borderRadius: 17,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
||||||
},
|
|
||||||
menuLabel: {
|
|
||||||
flex: 1,
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '800',
|
|
||||||
},
|
|
||||||
pressed: {
|
|
||||||
opacity: 0.76,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
|
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import '@/global.css';
|
|
||||||
|
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
export const Colors = {
|
export const Colors = {
|
||||||
|
|||||||
+12
-5
@@ -1,9 +1,16 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--font-display:
|
--font-display:
|
||||||
Spline Sans, Inter, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji,
|
Spline Sans, Inter, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji,
|
||||||
Segoe UI Symbol, Noto Color Emoji;
|
Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||||
--font-mono:
|
--font-mono:
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono,
|
||||||
--font-rounded: 'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif;
|
Courier New, monospace;
|
||||||
--font-serif: Georgia, 'Times New Roman', serif;
|
--font-rounded:
|
||||||
|
"SF Pro Rounded", "Hiragino Maru Gothic ProN", Meiryo, "MS PGothic",
|
||||||
|
sans-serif;
|
||||||
|
--font-serif: Georgia, "Times New Roman", serif;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useSyncExternalStore } from "react";
|
||||||
import { useColorScheme as useRNColorScheme } from 'react-native';
|
import { useColorScheme as useRNColorScheme } from "react-native";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To support static rendering, this value needs to be re-calculated on the client side for web
|
* To support static rendering, this value needs to be re-calculated on the client side for web
|
||||||
*/
|
*/
|
||||||
export function useColorScheme() {
|
export function useColorScheme() {
|
||||||
const [hasHydrated, setHasHydrated] = useState(false);
|
const hasHydrated = useSyncExternalStore(
|
||||||
|
() => () => {},
|
||||||
useEffect(() => {
|
() => true,
|
||||||
setHasHydrated(true);
|
() => false,
|
||||||
}, []);
|
);
|
||||||
|
|
||||||
const colorScheme = useRNColorScheme();
|
const colorScheme = useRNColorScheme();
|
||||||
|
|
||||||
if (hasHydrated) {
|
if (hasHydrated) {
|
||||||
return colorScheme;
|
return colorScheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'light';
|
return "light";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./src/**/*.{js,jsx,ts,tsx}", "./app/**/*.{js,jsx,ts,tsx}"],
|
||||||
|
presets: [require("nativewind/preset")],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
+3
-1
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "expo/tsconfig.base",
|
"extends": "expo/tsconfig.base",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"jsxImportSource": "nativewind",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".expo/types/**/*.ts",
|
".expo/types/**/*.ts",
|
||||||
"expo-env.d.ts"
|
"expo-env.d.ts",
|
||||||
|
"nativewind-env.d.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user