Merge branch 'main' of https://gitea.bowongai.com/bowong/expo-duooomi-app
This commit is contained in:
commit
167ae2f138
|
|
@ -1,19 +1,20 @@
|
||||||
import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'
|
import { FontAwesome, Fontisto, Ionicons } from '@expo/vector-icons'
|
||||||
import { Block, Text, Img, Input, VideoBox } from '@/@share/components'
|
import { useRouter } from 'expo-router'
|
||||||
import { imgPicker } from '@/@share/apis/imgPicker'
|
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import Svg, { Defs, Pattern, Rect, Circle } from 'react-native-svg'
|
import { ActivityIndicator } from 'react-native'
|
||||||
import { ScrollView } from 'react-native-gesture-handler'
|
import { ScrollView } from 'react-native-gesture-handler'
|
||||||
import { useAnimatedStyle } from 'react-native-reanimated'
|
import { useAnimatedStyle } from 'react-native-reanimated'
|
||||||
import { FontAwesome, Fontisto, Ionicons } from '@expo/vector-icons'
|
|
||||||
import { screenHeight, screenWidth } from '@/utils'
|
|
||||||
import { useRouter } from 'expo-router'
|
|
||||||
import { useRecommendedTemplates } from '@/hooks/data/use-recommended-templates'
|
|
||||||
import { useCategories } from '@/hooks/data/use-categories'
|
|
||||||
import { useTemplates } from '@/hooks/data/use-templates'
|
|
||||||
import { ActivityIndicator } from 'react-native'
|
|
||||||
import { ApiError } from '@/lib/types'
|
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
import Svg, { Circle, Defs, Pattern, Rect } from 'react-native-svg'
|
||||||
|
|
||||||
|
import { imgPicker } from '@/@share/apis/imgPicker'
|
||||||
|
import { Block, Img, Input, Text, VideoBox } from '@/@share/components'
|
||||||
import BannerSection from '@/components/BannerSection'
|
import BannerSection from '@/components/BannerSection'
|
||||||
|
import { useCategories } from '@/hooks/data/use-categories'
|
||||||
|
import { useRecommendedTemplates } from '@/hooks/data/use-recommended-templates'
|
||||||
|
import { useTemplates } from '@/hooks/data/use-templates'
|
||||||
|
import { type ApiError } from '@/lib/types'
|
||||||
|
import { screenHeight, screenWidth } from '@/utils'
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -43,7 +44,9 @@ export default function Index() {
|
||||||
const recommended = recommendedTemplates.data?.templates || []
|
const recommended = recommendedTemplates.data?.templates || []
|
||||||
const regular = templates.data?.templates || []
|
const regular = templates.data?.templates || []
|
||||||
|
|
||||||
const all = selectedCategoryId ? regular : [...recommended.map((r: RecommendedTemplate) => r.template).filter(Boolean), ...regular]
|
const all = selectedCategoryId
|
||||||
|
? regular
|
||||||
|
: [...recommended.map((r: RecommendedTemplate) => r.template).filter(Boolean), ...regular]
|
||||||
|
|
||||||
return all.map(
|
return all.map(
|
||||||
(t: any): Template => ({
|
(t: any): Template => ({
|
||||||
|
|
@ -128,17 +131,23 @@ export default function Index() {
|
||||||
<Header onSearch={handleSearch} />
|
<Header onSearch={handleSearch} />
|
||||||
|
|
||||||
<Block className="px-16px relative z-10 flex-1">
|
<Block className="px-16px relative z-10 flex-1">
|
||||||
<UploadSection meImg={meImg} friendImg={friendImg} onPickMe={onPickMe} onPickFriend={onPickFriend} />
|
<UploadSection friendImg={friendImg} meImg={meImg} onPickFriend={onPickFriend} onPickMe={onPickMe} />
|
||||||
<PromptSection prompt={prompt} onChangePrompt={setPrompt} />
|
<PromptSection prompt={prompt} onChangePrompt={setPrompt} />
|
||||||
{categoryList.length > 0 && <CategoryFilter categories={categoryList} selectedId={selectedCategoryId} onSelect={handleSelectCategory} />}
|
{categoryList.length > 0 && (
|
||||||
|
<CategoryFilter
|
||||||
|
categories={categoryList}
|
||||||
|
selectedId={selectedCategoryId}
|
||||||
|
onSelect={handleSelectCategory}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<TemplateSection
|
<TemplateSection
|
||||||
templates={displayTemplates}
|
|
||||||
selectedTemplateId={selectedTemplateId}
|
|
||||||
onSelectTemplate={handleSelectTemplate}
|
|
||||||
onRandom={handleRandom}
|
|
||||||
loading={isLoading}
|
|
||||||
error={hasError}
|
error={hasError}
|
||||||
|
loading={isLoading}
|
||||||
|
selectedTemplateId={selectedTemplateId}
|
||||||
|
templates={displayTemplates}
|
||||||
|
onRandom={handleRandom}
|
||||||
onRetry={handleRetry}
|
onRetry={handleRetry}
|
||||||
|
onSelectTemplate={handleSelectTemplate}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
@ -158,17 +167,17 @@ export default function Index() {
|
||||||
type BannerProps = { bgVideo: string }
|
type BannerProps = { bgVideo: string }
|
||||||
const Banner = memo<BannerProps>(function Banner({ bgVideo }) {
|
const Banner = memo<BannerProps>(function Banner({ bgVideo }) {
|
||||||
return (
|
return (
|
||||||
<Block className="absolute inset-0 bottom-0 left-0 right-0 top-0 z-0 overflow-hidden">
|
<Block className="absolute inset-0 z-0 overflow-hidden">
|
||||||
<VideoBox url={bgVideo} style={{ width: screenWidth, height: screenHeight }} />
|
<VideoBox style={{ width: screenWidth, height: screenHeight }} url={bgVideo} />
|
||||||
|
|
||||||
<Block className="absolute inset-0">
|
<Block className="absolute inset-0">
|
||||||
<Svg width="100%" height="100%" viewBox="0 0 400 800" preserveAspectRatio="none">
|
<Svg height="100%" preserveAspectRatio="none" viewBox="0 0 400 800" width="100%">
|
||||||
<Defs>
|
<Defs>
|
||||||
<Pattern id="dots" x="0" y="0" width="15" height="15" patternUnits="userSpaceOnUse">
|
<Pattern height="15" id="dots" patternUnits="userSpaceOnUse" width="15" x="0" y="0">
|
||||||
<Circle cx="2" cy="2" r="2" fill="#ffff00" opacity="0.05" />
|
<Circle cx="2" cy="2" fill="#ffff00" opacity="0.05" r="2" />
|
||||||
</Pattern>
|
</Pattern>
|
||||||
</Defs>
|
</Defs>
|
||||||
<Rect x="0" y="0" width="100%" height="100%" fill="url(#dots)" />
|
<Rect fill="url(#dots)" height="100%" width="100%" x="0" y="0" />
|
||||||
</Svg>
|
</Svg>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
|
|
@ -183,17 +192,17 @@ type HeaderProps = { onSearch: () => void }
|
||||||
const Header = memo<HeaderProps>(function Header({ onSearch }) {
|
const Header = memo<HeaderProps>(function Header({ onSearch }) {
|
||||||
return (
|
return (
|
||||||
<Block className="z-20 flex-row items-center justify-between py-[12px]">
|
<Block className="z-20 flex-row items-center justify-between py-[12px]">
|
||||||
<Block className="skew-x-[-12deg] flex-row items-center gap-[8px] border-[1px] border-white/20 bg-black px-[12px] py-[4px]">
|
<Block className="-skew-x-12 flex-row items-center gap-[8px] border border-white/20 bg-black px-[12px] py-[4px]">
|
||||||
<Block className="h-[8px] w-[8px] bg-[#FFE500]" />
|
<Block className="size-[8px] bg-accent" />
|
||||||
<Text className="font-700 skew-x-[12deg] text-[12px] tracking-wider text-white">创造终端</Text>
|
<Text className="font-700 skew-x-12 text-[12px] tracking-wider text-white">创造终端</Text>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
<Block className="items-center">
|
<Block className="items-center">
|
||||||
<Text className="font-700 text-[24px] italic tracking-tighter text-white">GEN_STUDIO</Text>
|
<Text className="font-700 text-[24px] italic tracking-tighter text-white">GEN_STUDIO</Text>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
<Block className="h-[32px] w-[32px] items-center justify-center" onClick={onSearch}>
|
<Block className="size-[32px] items-center justify-center" onClick={onSearch}>
|
||||||
<Ionicons name="search" size={20} color="white" />
|
<Ionicons color="white" name="search" size={20} />
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
)
|
)
|
||||||
|
|
@ -210,26 +219,26 @@ const UploadCard = memo<UploadCardProps>(function UploadCard({ variant, img, onP
|
||||||
return (
|
return (
|
||||||
<Block className="flex-1 overflow-hidden border-[3px] border-black bg-white" onClick={onPick}>
|
<Block className="flex-1 overflow-hidden border-[3px] border-black bg-white" onClick={onPick}>
|
||||||
{isMe ? (
|
{isMe ? (
|
||||||
<Block className="absolute left-[0px] top-[0px] z-10 border-b-[2px] border-r-[2px] border-black bg-[#FFE500] px-[8px] py-[2px]">
|
<Block className="absolute left-0 top-0 z-10 border-b-2 border-r-2 border-black bg-accent px-[8px] py-[2px]">
|
||||||
<Text className="text-[10px] font-[900] italic text-black">我</Text>
|
<Text className="text-[10px] font-[900] italic text-black">我</Text>
|
||||||
</Block>
|
</Block>
|
||||||
) : (
|
) : (
|
||||||
<Block className="absolute right-[0px] top-[0px] z-10 border-b-[2px] border-l-[2px] border-white bg-black px-[8px] py-[2px]">
|
<Block className="absolute right-0 top-0 z-10 border-b-2 border-l-2 border-white bg-black px-[8px] py-[2px]">
|
||||||
<Text className="text-[10px] font-[900] italic text-white">朋友</Text>
|
<Text className="text-[10px] font-[900] italic text-white">朋友</Text>
|
||||||
</Block>
|
</Block>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Block className="h-full w-full items-center justify-center bg-[#F3F4F6]">
|
<Block className="size-full items-center justify-center bg-[#F3F4F6]">
|
||||||
{img ? (
|
{img ? (
|
||||||
<Img src={img} className="h-full w-full" contentFit="cover" />
|
<Img className="size-full" contentFit="cover" src={img} />
|
||||||
) : (
|
) : (
|
||||||
<Block className="items-center">
|
<Block className="items-center">
|
||||||
{isMe ? (
|
{isMe ? (
|
||||||
<Block className="mb-[4px] h-[40px] w-[40px] items-center justify-center rounded-[20px] border-[2px] border-black bg-white">
|
<Block className="mb-[4px] size-[40px] items-center justify-center rounded-[20px] border-2 border-black bg-white">
|
||||||
<Text className="font-700 text-[16px] text-black">+</Text>
|
<Text className="font-700 text-[16px] text-black">+</Text>
|
||||||
</Block>
|
</Block>
|
||||||
) : (
|
) : (
|
||||||
<Block className="mb-[4px] h-[40px] w-[40px] items-center justify-center rounded-[20px] bg-black">
|
<Block className="mb-[4px] size-[40px] items-center justify-center rounded-[20px] bg-black">
|
||||||
<Text className="font-700 text-[16px] text-white">+</Text>
|
<Text className="font-700 text-[16px] text-white">+</Text>
|
||||||
</Block>
|
</Block>
|
||||||
)}
|
)}
|
||||||
|
|
@ -251,8 +260,8 @@ type UploadSectionProps = {
|
||||||
const UploadSection = memo<UploadSectionProps>(function UploadSection({ meImg, friendImg, onPickMe, onPickFriend }) {
|
const UploadSection = memo<UploadSectionProps>(function UploadSection({ meImg, friendImg, onPickMe, onPickFriend }) {
|
||||||
return (
|
return (
|
||||||
<Block className="z-10 flex h-[160px] w-full flex-row gap-x-[12px]">
|
<Block className="z-10 flex h-[160px] w-full flex-row gap-x-[12px]">
|
||||||
<UploadCard variant="me" img={meImg} onPick={onPickMe} />
|
<UploadCard img={meImg} variant="me" onPick={onPickMe} />
|
||||||
<UploadCard variant="friend" img={friendImg} onPick={onPickFriend} />
|
<UploadCard img={friendImg} variant="friend" onPick={onPickFriend} />
|
||||||
</Block>
|
</Block>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -263,21 +272,21 @@ type PromptSectionProps = {
|
||||||
}
|
}
|
||||||
const PromptSection = memo<PromptSectionProps>(function PromptSection({ prompt, onChangePrompt }) {
|
const PromptSection = memo<PromptSectionProps>(function PromptSection({ prompt, onChangePrompt }) {
|
||||||
return (
|
return (
|
||||||
<Block className="mt-[24px] skew-x-[-6deg]">
|
<Block className="mt-[24px] -skew-x-6">
|
||||||
<Block className="mb-[4px] flex-row items-center px-[4px]">
|
<Block className="mb-[4px] flex-row items-center px-[4px]">
|
||||||
<Block className="border-[1px] border-white/20 bg-black px-[8px] py-[2px]">
|
<Block className="border border-white/20 bg-black px-[8px] py-[2px]">
|
||||||
<Text className="text-[12px] font-[900] italic text-[#FFE500]">提示词</Text>
|
<Text className="text-[12px] font-[900] italic text-accent">提示词</Text>
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
<Block className="border-[3px] border-black bg-white p-[4px]">
|
<Block className="border-[3px] border-black bg-white p-[4px]">
|
||||||
<Input
|
<Input
|
||||||
|
multiline
|
||||||
|
className="font-700 min-h-[50px] w-full bg-transparent p-[8px] text-[14px] leading-[18px] text-black"
|
||||||
|
numberOfLines={2}
|
||||||
|
placeholder="描述画面细节..."
|
||||||
value={prompt}
|
value={prompt}
|
||||||
onChangeText={onChangePrompt}
|
onChangeText={onChangePrompt}
|
||||||
placeholder="描述画面细节..."
|
|
||||||
multiline
|
|
||||||
numberOfLines={2}
|
|
||||||
className="font-700 min-h-[50px] w-full bg-transparent px-[8px] py-[8px] text-[14px] leading-[18px] text-black"
|
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
@ -301,7 +310,7 @@ type TemplateItemProps = {
|
||||||
const TemplateItem = memo<TemplateItemProps>(function TemplateItem({ item, itemWidth, isSelected, onSelect }) {
|
const TemplateItem = memo<TemplateItemProps>(function TemplateItem({ item, itemWidth, isSelected, onSelect }) {
|
||||||
return (
|
return (
|
||||||
<Block
|
<Block
|
||||||
className={`relative border-[2px] ${isSelected ? 'border-accent' : 'border-black'}`}
|
className={`relative border-2 ${isSelected ? 'border-accent' : 'border-black'}`}
|
||||||
style={{
|
style={{
|
||||||
transform: [{ skewX: '-6deg' }],
|
transform: [{ skewX: '-6deg' }],
|
||||||
height: itemWidth,
|
height: itemWidth,
|
||||||
|
|
@ -311,18 +320,18 @@ const TemplateItem = memo<TemplateItemProps>(function TemplateItem({ item, itemW
|
||||||
}}
|
}}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
>
|
>
|
||||||
<Img src={item.image} className="h-full w-full" contentFit="cover" />
|
<Img className="size-full" contentFit="cover" src={item.image} />
|
||||||
|
|
||||||
{isSelected && <Block className="absolute inset-0 bg-black/60" />}
|
{isSelected && <Block className="absolute inset-0 bg-black/60" />}
|
||||||
{isSelected && <Block className="absolute inset-[-5px] border-[3px] border-accent" />}
|
{isSelected && <Block className="absolute inset-[-5px] border-[3px] border-accent" />}
|
||||||
|
|
||||||
<Block className="absolute inset-x-0 bottom-0 items-center bg-black/90 px-[4px] py-[4px]">
|
<Block className="absolute inset-x-0 bottom-0 items-center bg-black/90 p-[4px]">
|
||||||
<Text className={`text-[8px] font-[700] italic ${isSelected ? 'text-accent' : 'text-white'}`}>{item.name}</Text>
|
<Text className={`text-[8px] font-[700] italic ${isSelected ? 'text-accent' : 'text-white'}`}>{item.name}</Text>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
{item.type === 'video' && (
|
{item.type === 'video' && (
|
||||||
<Block className="rounded-4px absolute right-[4px] top-[4px] rounded-[4px] border-[1px] border-white/50 bg-black p-[4px]">
|
<Block className="rounded-4px absolute right-[4px] top-[4px] rounded-[4px] border border-white/50 bg-black p-[4px]">
|
||||||
<Fontisto name="youtube-play" size={8} color="white" />
|
<Fontisto color="white" name="youtube-play" size={8} />
|
||||||
</Block>
|
</Block>
|
||||||
)}
|
)}
|
||||||
</Block>
|
</Block>
|
||||||
|
|
@ -337,7 +346,7 @@ type CategoryChipProps = {
|
||||||
const CategoryChip = memo<CategoryChipProps>(function CategoryChip({ name, isSelected, onSelect }) {
|
const CategoryChip = memo<CategoryChipProps>(function CategoryChip({ name, isSelected, onSelect }) {
|
||||||
return (
|
return (
|
||||||
<Block
|
<Block
|
||||||
className={`skew-x-[-6deg] border-[2px] px-[12px] py-[4px] ${isSelected ? 'border-accent bg-accent' : 'border-black bg-white'}`}
|
className={`-skew-x-6 border-2 px-[12px] py-[4px] ${isSelected ? 'border-accent bg-accent' : 'border-black bg-white'}`}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
>
|
>
|
||||||
<Text className={`text-[10px] font-[900] italic ${isSelected ? 'text-black' : 'text-black/60'}`}>{name}</Text>
|
<Text className={`text-[10px] font-[900] italic ${isSelected ? 'text-black' : 'text-black/60'}`}>{name}</Text>
|
||||||
|
|
@ -346,17 +355,22 @@ const CategoryChip = memo<CategoryChipProps>(function CategoryChip({ name, isSel
|
||||||
})
|
})
|
||||||
|
|
||||||
type CategoryFilterProps = {
|
type CategoryFilterProps = {
|
||||||
categories: Array<{ id: string; name: string }>
|
categories: { id: string; name: string }[]
|
||||||
selectedId: string
|
selectedId: string
|
||||||
onSelect: (id: string) => void
|
onSelect: (id: string) => void
|
||||||
}
|
}
|
||||||
const CategoryFilter = memo<CategoryFilterProps>(function CategoryFilter({ categories, selectedId, onSelect }) {
|
const CategoryFilter = memo<CategoryFilterProps>(function CategoryFilter({ categories, selectedId, onSelect }) {
|
||||||
return (
|
return (
|
||||||
<Block className="mt-[16px]">
|
<Block className="mt-[16px]">
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8 }}>
|
<ScrollView horizontal contentContainerStyle={{ gap: 8 }} showsHorizontalScrollIndicator={false}>
|
||||||
<CategoryChip name="全部" isSelected={selectedId === ''} onSelect={() => onSelect('')} />
|
<CategoryChip isSelected={selectedId === ''} name="全部" onSelect={() => onSelect('')} />
|
||||||
{categories.map((cat) => (
|
{categories.map((cat) => (
|
||||||
<CategoryChip key={cat.id} name={cat.name} isSelected={selectedId === cat.id} onSelect={() => onSelect(cat.id)} />
|
<CategoryChip
|
||||||
|
key={cat.id}
|
||||||
|
isSelected={selectedId === cat.id}
|
||||||
|
name={cat.name}
|
||||||
|
onSelect={() => onSelect(cat.id)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
@ -393,25 +407,32 @@ const TemplateSection = memo<TemplateSectionProps>(function TemplateSection({
|
||||||
return (
|
return (
|
||||||
<Block className="mt-[16px]">
|
<Block className="mt-[16px]">
|
||||||
<Block className="flex-row items-center justify-between">
|
<Block className="flex-row items-center justify-between">
|
||||||
<Block animated style={style} className="flex-row items-center gap-[8px] border-[2px] border-black bg-[#FFE500] px-[12px] py-[4px]">
|
<Block
|
||||||
<FontAwesome name="film" size={16} color="black" />
|
animated
|
||||||
|
className="flex-row items-center gap-[8px] border-2 border-black bg-accent px-[12px] py-[4px]"
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<FontAwesome color="black" name="film" size={16} />
|
||||||
<Text className="text-[10px] font-[900] italic text-black">视频模版</Text>
|
<Text className="text-[10px] font-[900] italic text-black">视频模版</Text>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
<Block className="skew-x-[-6deg] items-center justify-center border-[2px] border-black bg-white px-[8px] py-[4px]" onClick={onRandom}>
|
<Block
|
||||||
|
className="-skew-x-6 items-center justify-center border-2 border-black bg-white px-[8px] py-[4px]"
|
||||||
|
onClick={onRandom}
|
||||||
|
>
|
||||||
<Text className="text-[10px] font-[900] italic text-black">换一波</Text>
|
<Text className="text-[10px] font-[900] italic text-black">换一波</Text>
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Block className="mt-[32px] items-center justify-center py-[40px]">
|
<Block className="mt-[32px] items-center justify-center py-[40px]">
|
||||||
<ActivityIndicator size="large" color="#FFE500" />
|
<ActivityIndicator color="#FFE500" size="large" />
|
||||||
</Block>
|
</Block>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<Block className="mt-[32px] items-center justify-center py-[40px]">
|
<Block className="mt-[32px] items-center justify-center py-[40px]">
|
||||||
<Text className="mb-[8px] text-[14px] text-white/60">加载失败</Text>
|
<Text className="mb-[8px] text-[14px] text-white/60">加载失败</Text>
|
||||||
{onRetry && (
|
{onRetry && (
|
||||||
<Block className="skew-x-[-6deg] border-[2px] border-white bg-white px-[16px] py-[6px]" onClick={onRetry}>
|
<Block className="-skew-x-6 border-2 border-white bg-white px-[16px] py-[6px]" onClick={onRetry}>
|
||||||
<Text className="text-[12px] font-[900] italic text-black">重试</Text>
|
<Text className="text-[12px] font-[900] italic text-black">重试</Text>
|
||||||
</Block>
|
</Block>
|
||||||
)}
|
)}
|
||||||
|
|
@ -424,7 +445,15 @@ const TemplateSection = memo<TemplateSectionProps>(function TemplateSection({
|
||||||
<Block className="mt-[16px] flex-row flex-wrap gap-[12px]">
|
<Block className="mt-[16px] flex-row flex-wrap gap-[12px]">
|
||||||
{templates.map((item) => {
|
{templates.map((item) => {
|
||||||
const isSelected = selectedTemplateId === item.id
|
const isSelected = selectedTemplateId === item.id
|
||||||
return <TemplateItem key={item.id} item={item} itemWidth={itemWidth} isSelected={isSelected} onSelect={() => onSelectTemplate(item)} />
|
return (
|
||||||
|
<TemplateItem
|
||||||
|
key={item.id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
item={item}
|
||||||
|
itemWidth={itemWidth}
|
||||||
|
onSelect={() => onSelectTemplate(item)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
})}
|
})}
|
||||||
</Block>
|
</Block>
|
||||||
)}
|
)}
|
||||||
|
|
@ -436,12 +465,12 @@ type GenerateSectionProps = { onGenerate: () => void }
|
||||||
const GenerateSection = memo<GenerateSectionProps>(function GenerateSection({ onGenerate }) {
|
const GenerateSection = memo<GenerateSectionProps>(function GenerateSection({ onGenerate }) {
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
return (
|
return (
|
||||||
<Block className="absolute bottom-[96px] left-[16px] right-[16px] z-40" style={{ paddingBottom: insets.bottom }}>
|
<Block className="absolute inset-x-[16px] bottom-[96px] z-40" style={{ paddingBottom: insets.bottom }}>
|
||||||
<Block style={{ transform: [{ skewX: '-6deg' }] }}>
|
<Block style={{ transform: [{ skewX: '-6deg' }] }}>
|
||||||
<Block
|
<Block
|
||||||
onClick={onGenerate}
|
className={`relative flex-row items-center justify-between border-[3px] border-black bg-accent py-[8px]`}
|
||||||
className={`relative flex-row items-center justify-between border-[3px] border-black bg-[#FFE500] py-[8px]`}
|
|
||||||
style={{ paddingHorizontal: 24 }}
|
style={{ paddingHorizontal: 24 }}
|
||||||
|
onClick={onGenerate}
|
||||||
>
|
>
|
||||||
{/* 左侧文字 */}
|
{/* 左侧文字 */}
|
||||||
<Block className="z-10 flex-col">
|
<Block className="z-10 flex-col">
|
||||||
|
|
@ -450,9 +479,9 @@ const GenerateSection = memo<GenerateSectionProps>(function GenerateSection({ on
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
{/* 右侧 Goo 标签 */}
|
{/* 右侧 Goo 标签 */}
|
||||||
<Block className="z-10 flex-row items-center gap-[8px] border-[2px] border-black bg-black px-[12px] py-[4px]">
|
<Block className="z-10 flex-row items-center gap-[8px] border-2 border-black bg-black px-[12px] py-[4px]">
|
||||||
<Ionicons name="flash" size={14} color="#FFE500" />
|
<Ionicons color="#FFE500" name="flash" size={14} />
|
||||||
<Text className="text-[14px] font-black italic text-[#FFE500]">3 Goo</Text>
|
<Text className="text-[14px] font-black italic text-accent">3 Goo</Text>
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,21 @@ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from '
|
||||||
import { ActivityIndicator, RefreshControl, TextInput } from 'react-native'
|
import { ActivityIndicator, RefreshControl, TextInput } from 'react-native'
|
||||||
|
|
||||||
import BannerSection from '@/components/BannerSection'
|
import BannerSection from '@/components/BannerSection'
|
||||||
import { useAigcTask } from '@/hooks/actions/use-aigc-task'
|
import { useTemplateActions } from '@/hooks/actions/use-template-actions'
|
||||||
import { useAuth } from '@/hooks/core/use-auth'
|
import { useAuth } from '@/hooks/core/use-auth'
|
||||||
import { useUserBalance } from '@/hooks/core/use-user-balance'
|
import { useUserBalance } from '@/hooks/core/use-user-balance'
|
||||||
|
import { useTemplates } from '@/hooks/data'
|
||||||
import { useFavoriteTemplates } from '@/hooks/data/use-favorite-templates'
|
import { useFavoriteTemplates } from '@/hooks/data/use-favorite-templates'
|
||||||
import { usePublicTemplates } from '@/hooks/data/use-public-templates'
|
import { usePublicTemplates } from '@/hooks/data/use-public-templates'
|
||||||
import { useTemplates } from '@/hooks/data/use-templates'
|
|
||||||
import { screenWidth } from '@/utils'
|
import { screenWidth } from '@/utils'
|
||||||
import { cn } from '@/utils/cn'
|
import { cn } from '@/utils/cn'
|
||||||
|
|
||||||
|
const BACKGROUND_VIDEOS = [
|
||||||
|
'https://cdn.roasmax.cn/material/b46f380532e14cf58dd350dbacc7c34a.mp4',
|
||||||
|
'https://cdn.roasmax.cn/material/992e6c5d940c42feb71c27e556b754c0.mp4',
|
||||||
|
'https://cdn.roasmax.cn/material/e4947477843f4067be7c37569a33d17b.mp4',
|
||||||
|
]
|
||||||
|
|
||||||
type MediaItem = {
|
type MediaItem = {
|
||||||
id: string
|
id: string
|
||||||
type: 'image' | 'video'
|
type: 'image' | 'video'
|
||||||
|
|
@ -324,7 +330,7 @@ export default function Sync() {
|
||||||
execute: loadPublicTemplates,
|
execute: loadPublicTemplates,
|
||||||
} = usePublicTemplates()
|
} = usePublicTemplates()
|
||||||
const { data: favoritesData, loading: favoritesLoading, execute: loadFavorites } = useFavoriteTemplates()
|
const { data: favoritesData, loading: favoritesLoading, execute: loadFavorites } = useFavoriteTemplates()
|
||||||
const { submitTask, startPolling } = useAigcTask()
|
const { runTemplate } = useTemplateActions()
|
||||||
|
|
||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
|
|
||||||
|
|
@ -365,7 +371,7 @@ export default function Sync() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated && user?.id) {
|
if (isAuthenticated && user?.id) {
|
||||||
loadBalance(user.id)
|
loadBalance()
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, user?.id])
|
}, [isAuthenticated, user?.id])
|
||||||
|
|
||||||
|
|
@ -604,39 +610,25 @@ export default function Sync() {
|
||||||
onCancel={() => Toast.hideModal()}
|
onCancel={() => Toast.hideModal()}
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
Toast.hideModal()
|
Toast.hideModal()
|
||||||
|
// 要传 扣费返回的凭证
|
||||||
const { taskId, error } = await submitTask({
|
const { generationId, error } = await runTemplate({
|
||||||
model_name: 'default',
|
templateId: selectedItem.id,
|
||||||
prompt: `生成与 ${selectedItem.id} 相似的内容`,
|
data: {},
|
||||||
img_url: selectedItem.url,
|
originalUrl: selectedItem.url,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (error) {
|
if (error || !generationId) {
|
||||||
Toast.show({ title: `提交失败: ${error.message}` })
|
Toast.show({ title: error?.message || '生成失败' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (taskId) {
|
Toast.show({ title: '生成成功!' })
|
||||||
Toast.show({
|
if (user?.id) loadBalance()
|
||||||
title: '任务提交成功,正在生成中...',
|
|
||||||
duration: 30e3,
|
|
||||||
})
|
|
||||||
startPolling(
|
|
||||||
taskId,
|
|
||||||
(result) => {
|
|
||||||
Toast.show({ title: '生成成功!' })
|
|
||||||
if (user?.id) loadBalance(user.id)
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
Toast.show({ title: `生成失败: ${error.message}` })
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
}, [isAuthenticated, balance, selectedItem, submitTask, startPolling, user?.id, loadBalance])
|
}, [isAuthenticated, balance, selectedItem, runTemplate, user?.id, loadBalance])
|
||||||
|
|
||||||
const openSearch = useCallback(() => setIsSearchOpen(true), [])
|
const openSearch = useCallback(() => setIsSearchOpen(true), [])
|
||||||
const closeSearch = useCallback(() => {
|
const closeSearch = useCallback(() => {
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ export default function GenerateVideoScreen() {
|
||||||
])
|
])
|
||||||
|
|
||||||
if (user?.id) {
|
if (user?.id) {
|
||||||
loadBalance(user.id)
|
loadBalance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export default function GenerationRecordScreen() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.id) {
|
if (user?.id) {
|
||||||
loadBalance(user.id)
|
loadBalance()
|
||||||
}
|
}
|
||||||
}, [user?.id])
|
}, [user?.id])
|
||||||
|
|
||||||
|
|
@ -110,7 +110,7 @@ export default function GenerationRecordScreen() {
|
||||||
} else {
|
} else {
|
||||||
Alert.alert('生成成功', '视频已生成完成')
|
Alert.alert('生成成功', '视频已生成完成')
|
||||||
if (user?.id) {
|
if (user?.id) {
|
||||||
loadBalance(user.id)
|
loadBalance()
|
||||||
}
|
}
|
||||||
if (newGeneration?.id) {
|
if (newGeneration?.id) {
|
||||||
router.replace({
|
router.replace({
|
||||||
|
|
@ -315,7 +315,7 @@ const styles = StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
gap:4,
|
gap: 4,
|
||||||
},
|
},
|
||||||
categoryIcon: {
|
categoryIcon: {
|
||||||
width: 16,
|
width: 16,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue