├── .eslintrc.json ├── public ├── ads.txt ├── client_hero.png ├── vercel.svg ├── next.svg └── sw.js ├── src ├── app │ ├── favicon.ico │ ├── [locale] │ │ ├── leaderboard │ │ │ ├── components │ │ │ │ ├── BackButton.tsx │ │ │ │ └── TimeFilter.tsx │ │ │ ├── fullranking │ │ │ │ └── [type] │ │ │ │ │ ├── components │ │ │ │ │ └── Pagination.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── topic │ │ │ │ └── [topic_id] │ │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── privacy-policy │ │ │ └── page.tsx │ │ ├── terms-conditions │ │ │ └── page.tsx │ │ ├── changelog │ │ │ └── page.tsx │ │ ├── roadmap │ │ │ └── page.tsx │ │ ├── components │ │ │ ├── FAQSection.tsx │ │ │ ├── BackToTop.tsx │ │ │ ├── Footer.tsx │ │ │ ├── SelectDraft.tsx │ │ │ ├── NovelList.tsx │ │ │ └── DraftSetting.tsx │ │ ├── layout.tsx │ │ ├── copilot │ │ │ ├── dashboard │ │ │ │ ├── page.tsx │ │ │ │ └── dashboard.tsx │ │ │ ├── download │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── donate │ │ │ └── page.tsx │ ├── globals.css │ └── api │ │ └── generate │ │ └── route.ts ├── i18n │ ├── i18n-config.ts │ ├── i18n.ts │ └── locales │ │ ├── zh_CN.json │ │ └── en_US.json ├── components │ ├── ChangeLanguage │ │ ├── ChangeLanguageItem │ │ │ └── index.jsx │ │ ├── ChangeLanguageDropdown.tsx │ │ └── index.tsx │ ├── Chatway.tsx │ ├── GoogleAnalytics.tsx │ ├── TawkChat.tsx │ ├── Monetag │ │ ├── NativeBanner.tsx │ │ └── InPagePush.tsx │ └── Navbar │ │ └── index.tsx ├── hooks │ ├── useLocale.ts │ └── useLocaleClient.ts ├── content │ ├── roadmap_zh_CN.mdx │ ├── roadmap_en_US.mdx │ ├── changelog_zh_CN.mdx │ ├── privacy_zh_CN.mdx │ ├── changelog_en_US.mdx │ ├── terms_zh_CN.mdx │ ├── privacy_en_US.mdx │ └── terms_en_US.mdx ├── jianying │ └── effects │ │ ├── transitions.ts │ │ └── animations.ts ├── middleware.ts └── styles │ └── markdown-dark.css ├── postcss.config.js ├── .vscode └── settings.json ├── next.config.js ├── .gitignore ├── tsconfig.json ├── tailwind.config.ts ├── package.json ├── README.md └── next-sitemap.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-5771766328963997, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iHunterDev/JianYingProBatchKeyframe/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/client_hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iHunterDev/JianYingProBatchKeyframe/HEAD/public/client_hero.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": [ 3 | "src/i18n", 4 | "src/i18n/locales" 5 | ], 6 | "i18n-ally.keystyle": "nested" 7 | } -------------------------------------------------------------------------------- /src/i18n/i18n-config.ts: -------------------------------------------------------------------------------- 1 | export const locales = ['zh_CN', 'en_US']; 2 | export const defaultLocale = 'zh_CN' 3 | export const localesName = { 4 | 'zh_CN': '🇨🇳 中文', 5 | 'en_US': '🇺🇸 English', 6 | } -------------------------------------------------------------------------------- /src/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import {getRequestConfig} from 'next-intl/server'; 2 | 3 | export default getRequestConfig(async ({locale}) => ({ 4 | messages: (await import(`./locales/${locale}.json`)).default 5 | })); -------------------------------------------------------------------------------- /src/components/ChangeLanguage/ChangeLanguageItem/index.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Dropdown } from 'flowbite-react'; 4 | 5 | function ChangeLanguageItem({ children }) { 6 | return ( 7 | { children } 8 | ); 9 | } 10 | 11 | export default ChangeLanguageItem -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const withNextIntl = require('next-intl/plugin')( 3 | // This is the default (also the `src` folder is supported out of the box) 4 | './src/i18n/i18n.ts' 5 | ); 6 | 7 | const nextConfig = {} 8 | 9 | module.exports = withNextIntl(nextConfig); 10 | -------------------------------------------------------------------------------- /src/hooks/useLocale.ts: -------------------------------------------------------------------------------- 1 | import { locales, defaultLocale } from '@/i18n/i18n-config'; 2 | 3 | export function useLocale(currentLocale: string) { 4 | const getLocalizedHref = (path: string) => { 5 | return currentLocale === defaultLocale ? path : `/${currentLocale}${path}`; 6 | }; 7 | 8 | return { currentLocale, getLocalizedHref }; 9 | } -------------------------------------------------------------------------------- /src/components/Chatway.tsx: -------------------------------------------------------------------------------- 1 | import Script from "next/script"; 2 | 3 | export default function TawkChat() { 4 | return ( 5 | <> 6 | {/* Start of Chatway Script */} 7 | 8 | {/* End of Chatway Script */} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/content/roadmap_zh_CN.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 功能开发计划 3 | --- 4 | 5 | # 功能开发计划 6 | 7 | ---- 8 | 9 | ## 等待开发 10 | - AI 字幕一键校对 11 | - 视频转绿幕视频 12 | - 提取字幕功能 13 | - 视频转图片功能 14 | - AI 扩图功能 15 | 16 | ## 2024年9月 17 | - [✅] 支持选择图片比例,如:让16:9的图片在9:16视频中全屏铺满; 18 | - [✅] 支持自定义入场动画; 19 | - [✅] 将固定转场效果调整为随机入场动画; 20 | - [✅] 支持设置动画速度,用户可调整入场动画的移动速度。 21 | - [✅] 支持设置关键帧速度 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/jianying/effects/transitions.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | 3 | export const Transitions = { 4 | "11387229": { 5 | category_id: "40427", 6 | category_name: "叠化", 7 | duration: 1200000, 8 | effect_id: "11387229", 9 | id: uuidv4().toLocaleUpperCase(), 10 | is_overlap: true, 11 | name: "雾化", 12 | platform: "all", 13 | request_id: "202309291644056EE2BA652126078C5561", 14 | resource_id: "7216171159589491259", 15 | type: "transition", 16 | }, 17 | } -------------------------------------------------------------------------------- /src/app/[locale]/leaderboard/components/BackButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useRouter } from 'next/navigation' 4 | import { useTranslations } from 'next-intl' 5 | 6 | export default function BackButton() { 7 | const router = useRouter() 8 | const t = useTranslations('Leaderboard.topicDetails') 9 | 10 | return ( 11 | 17 | ) 18 | } -------------------------------------------------------------------------------- /src/components/ChangeLanguage/ChangeLanguageDropdown.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Dropdown } from 'flowbite-react'; 4 | export function ChangeLanguageDropdownItem({ children }: any) { 5 | return ( 6 | {children} 7 | ); 8 | } 9 | 10 | export function ChangeLanguageDropdown({children, label} : { children: React.ReactNode, label: string }) { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # next-sitemap.js 38 | /public/robots.txt 39 | /public/sitemap* -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/GoogleAnalytics.tsx: -------------------------------------------------------------------------------- 1 | import Script from "next/script"; 2 | 3 | export default function GoogleAnalytics() { 4 | return ( 5 | <> 6 | {/* */} 7 | 10 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/hooks/useLocaleClient.ts: -------------------------------------------------------------------------------- 1 | import { usePathname } from 'next/navigation'; 2 | import { locales, defaultLocale } from '@/i18n/i18n-config'; 3 | 4 | export function useLocale() { 5 | const pathname = usePathname(); 6 | const segments = pathname.split('/'); 7 | 8 | // 检查第一个路径段是否是有效的语言代码 9 | const localeFromPath = segments[1]; 10 | const currentLocale = locales.includes(localeFromPath) ? localeFromPath : defaultLocale; 11 | 12 | const getLocalizedHref = (path: string) => { 13 | return currentLocale === defaultLocale ? path : `/${currentLocale}${path}`; 14 | }; 15 | 16 | return { currentLocale, getLocalizedHref }; 17 | } -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | /* --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; */ 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import createMiddleware from 'next-intl/middleware'; 2 | import { locales, defaultLocale } from './i18n/i18n-config'; 3 | export default createMiddleware({ 4 | // A list of all locales that are supported 5 | locales: locales, 6 | 7 | // If this locale is matched, pathnames work without a prefix (e.g. `/about`) 8 | defaultLocale: defaultLocale, 9 | 10 | localePrefix: 'as-needed', 11 | localeDetection: false, 12 | }); 13 | 14 | export const config = { 15 | // Skip all paths that should not be internationalized. This example skips 16 | // certain folders and all pathnames with a dot (e.g. favicon.ico) 17 | matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'] 18 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/content/roadmap_en_US.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Feature Development Roadmap 3 | --- 4 | 5 | # Feature Development Roadmap 6 | 7 | ---- 8 | 9 | ## Pending Development 10 | - AI Subtitle One-Click Revision 11 | - Video Transcoding to Green Screen Video 12 | - Extract Subtitle Function 13 | - Video Transcoding to Image 14 | - AI Image Enlargement Function 15 | 16 | ## September 2024 17 | - [✅] Support for selecting image aspect ratio, e.g., allowing a 16:9 image to fill the screen in a 9:16 video; 18 | - [✅] Support for custom entrance animations; 19 | - [✅] Adjust fixed transition effects to random entrance animations; 20 | - [✅] Support for setting animation speed, allowing users to adjust the movement speed of entrance animations. 21 | - [✅] Support for setting keyframe speed 22 | -------------------------------------------------------------------------------- /src/components/TawkChat.tsx: -------------------------------------------------------------------------------- 1 | import Script from "next/script"; 2 | 3 | export default function TawkChat() { 4 | return ( 5 | <> 6 | {/* Start of Tawk.to Script */} 7 | 20 | {/* End of Tawk.to Script */} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 8 | // 添加下面这行来包含 Flowbite 组件 9 | 'node_modules/flowbite-react/**/*.{js,jsx,ts,tsx}' 10 | ], 11 | theme: { 12 | extend: { 13 | backgroundImage: { 14 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 15 | 'gradient-conic': 16 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 17 | }, 18 | }, 19 | }, 20 | plugins: [ 21 | // 添加 Flowbite 插件 22 | require('flowbite/plugin'), 23 | require('daisyui'), 24 | ], 25 | } 26 | 27 | export default config 28 | -------------------------------------------------------------------------------- /src/components/Monetag/NativeBanner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import Script from "next/script"; 5 | 6 | export default function NativeBanner() { 7 | const [loadScript, setLoadScript] = useState(false); 8 | useEffect(() => { 9 | 10 | if (process.env.NODE_ENV === 'production') { 11 | const timer = setTimeout(() => { 12 | setLoadScript(true); 13 | }, 5000); 14 | 15 | return () => clearTimeout(timer); 16 | } 17 | }, []); 18 | 19 | if (process.env.NODE_ENV !== 'production') { 20 | return null; 21 | } 22 | 23 | return ( 24 | <> 25 | {loadScript && ( 26 | 38 | )} 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/content/changelog_zh_CN.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 更新日志 3 | --- 4 | 5 | # 更新日志 6 | 7 | ---- 8 | 9 | ## 2024年11月 10 | - 视频时长处理逻辑更新:优化了视频时长的处理逻辑,提升了系统的稳定性 11 | - 添加一个开关,决定是否添加入场动画,默认开启 12 | - 添加抖音热门小说排行榜 13 | 14 | ## 2024年10月 15 | - 优化客户端读取不到草稿问题(添加自动检测草稿位置功能) 16 | - 添加隐私协议和用户协议 17 | - 优化首页导航和页脚 18 | 19 | ## 2024年9月 20 | - 支持图片的多种宽高比选择,提升了用户生成图片的灵活性。 21 | - 新增视频片段的动态缩放功能,优化了视频剪辑的用户体验。 22 | - 将固定转场效果调整为随机入场动画; 23 | - 支持自定义入场动画; 24 | - 支持设置动画速度,用户可调整入场动画的移动速度。 25 | - 支持设置关键帧速度 26 | - 标准化了文件上传和 JSON 处理,提高了上传和数据解析的稳定性。 27 | - 支持大小关键帧 28 | 29 | ## 2024年8月 30 | - 添加了新的实时聊天工具,改进了用户沟通体验。 31 | - 把项目从 vercel 迁移到自己的服务器 32 | - 给网站添加了广告,收回每年的域名成本 33 | - 修复了一键关键帧的部分情况出现黑边的问题 34 | 35 | ## 2024年7月 36 | - 为不同语言区域增加了动态元数据生成功能。 37 | - 优化了网站的分析工具,提升了性能。 38 | 39 | ## 2024年6月 40 | - 改进了下载页面的用户体验,添加了通知和下载按钮。 41 | - 更新了下载链接,支持更多的设备和平台。 42 | - 引入了新的实时聊天功能,提升了用户支持。 43 | 44 | ## 2024年5月 45 | - 新增了 Copilot 的下载页面,支持多平台下载。 46 | - 添加了教程视频和步骤,帮助用户更好地使用 Copilot。 47 | 48 | ## 2024年1月 - 2023年11月 49 | - 新增了站点地图和内链功能,提升了网站的 SEO 表现。 50 | - 优化了页面加载速度和用户界面,改善了整体用户体验。 51 | - 添加了捐赠页面和预发布页面。 52 | 53 | ## 2023年10月 54 | - **首个版本上线**:推出了产品的初始版本,包含了核心功能,如页面导航、语言切换、文件下载和基础分析工具。 55 | 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jianyingpro-batch-keyframe", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "postbuild": "next-sitemap" 11 | }, 12 | "dependencies": { 13 | "flowbite": "^2.5.2", 14 | "flowbite-react": "^0.6.4", 15 | "gray-matter": "^4.0.3", 16 | "next": "^14.2.3", 17 | "next-intl": "^3.0.0-beta.19", 18 | "next-mdx-remote": "^5.0.0", 19 | "next-plausible": "^3.12.0", 20 | "next-sitemap": "^4.2.3", 21 | "nextjs-toploader": "^1.6.12", 22 | "react": "^18", 23 | "react-dom": "^18", 24 | "react-icons": "^5.3.0", 25 | "react-loading": "^2.0.3", 26 | "sweetalert2": "^11.11.1", 27 | "uuid": "^9.0.1" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^20", 31 | "@types/react": "^18", 32 | "@types/react-dom": "^18", 33 | "@types/uuid": "^10.0.0", 34 | "autoprefixer": "^10", 35 | "daisyui": "^4.12.14", 36 | "eslint": "^8", 37 | "eslint-config-next": "13.5.5", 38 | "postcss": "^8", 39 | "tailwindcss": "^3", 40 | "typescript": "^5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/[locale]/leaderboard/components/TimeFilter.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations } from 'next-intl/server'; 2 | import Link from 'next/link'; 3 | 4 | interface TimeFilterProps { 5 | currentDays: number; 6 | } 7 | 8 | export default async function TimeFilter({ currentDays }: TimeFilterProps) { 9 | const t = await getTranslations('Leaderboard'); 10 | 11 | const timeOptions = [ 12 | { days: 7, label: t('last7Days') }, 13 | { days: 14, label: t('last14Days') }, 14 | { days: 30, label: t('last30Days') }, 15 | ]; 16 | 17 | return ( 18 |
19 | {t('timeRange')}: 20 |
21 | {timeOptions.map((option) => ( 22 | 31 | {option.label} 32 | 33 | ))} 34 |
35 |
36 | ); 37 | } -------------------------------------------------------------------------------- /src/app/[locale]/privacy-policy/page.tsx: -------------------------------------------------------------------------------- 1 | import { MDXRemote } from "next-mdx-remote/rsc"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | import "@/styles/markdown-dark.css"; 5 | import matter from "gray-matter"; 6 | 7 | export default async function PrivacyPolicy({ params: { locale } } : { params: { locale: string } }) { 8 | const postsDirectory = path.join(process.cwd(), "src/content"); 9 | const fileContents = fs.readFileSync( 10 | path.join(postsDirectory, `privacy_${locale}.mdx`), 11 | "utf8" 12 | ); 13 | // 使用 gray-matter 解析 frontmatter 和内容 14 | const { data: frontmatter, content } = matter(fileContents); 15 | return ( 16 | <> 17 |
18 | 23 |
24 | 25 |
26 | 27 |
28 |
29 |
30 | 31 | ); 32 | } -------------------------------------------------------------------------------- /src/app/[locale]/terms-conditions/page.tsx: -------------------------------------------------------------------------------- 1 | import { MDXRemote } from "next-mdx-remote/rsc"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | import "@/styles/markdown-dark.css"; 5 | import matter from "gray-matter"; 6 | 7 | export default async function TermsConditions({ params: { locale } } : { params: { locale: string } }) { 8 | const postsDirectory = path.join(process.cwd(), "src/content"); 9 | const fileContents = fs.readFileSync( 10 | path.join(postsDirectory, `terms_${locale}.mdx`), 11 | "utf8" 12 | ); 13 | // 使用 gray-matter 解析 frontmatter 和内容 14 | const { data: frontmatter, content } = matter(fileContents); 15 | return ( 16 | <> 17 |
18 | 23 |
24 | 25 |
26 | 27 |
28 |
29 |
30 | 31 | ); 32 | } -------------------------------------------------------------------------------- /src/app/[locale]/changelog/page.tsx: -------------------------------------------------------------------------------- 1 | import { MDXRemote } from "next-mdx-remote/rsc"; 2 | // import "github-markdown-css/github-markdown.css"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | import "@/styles/markdown-dark.css"; 6 | import matter from "gray-matter"; 7 | 8 | export default async function Changelog({ params: { locale } } : { params: { locale: string } }) { 9 | const postsDirectory = path.join(process.cwd(), "src/content"); 10 | const fileContents = fs.readFileSync( 11 | path.join(postsDirectory, `changelog_${locale}.mdx`), 12 | "utf8" 13 | ); 14 | // 使用 gray-matter 解析 frontmatter 和内容 15 | const { data: frontmatter, content } = matter(fileContents); 16 | return ( 17 | <> 18 |
19 | 24 |
25 | 26 |
27 | 28 |
29 |
30 |
31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/[locale]/roadmap/page.tsx: -------------------------------------------------------------------------------- 1 | import { MDXRemote } from "next-mdx-remote/rsc"; 2 | // import "github-markdown-css/github-markdown.css"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | import "@/styles/markdown-dark.css"; 6 | import matter from "gray-matter"; 7 | 8 | export default async function Changelog({ params: { locale } } : { params: { locale: string } }) { 9 | const postsDirectory = path.join(process.cwd(), "src/content"); 10 | const fileContents = fs.readFileSync( 11 | path.join(postsDirectory, `roadmap_${locale}.mdx`), 12 | "utf8" 13 | ); 14 | // 使用 gray-matter 解析 frontmatter 和内容 15 | const { data: frontmatter, content } = matter(fileContents); 16 | return ( 17 | <> 18 |
19 | 24 |
25 | 26 |
27 | 28 |
29 |
30 |
31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/[locale]/leaderboard/fullranking/[type]/components/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useTranslations } from 'next-intl'; 3 | 4 | interface PaginationProps { 5 | currentPage: number; 6 | hasNextPage: boolean; 7 | type: string; 8 | days: number; 9 | } 10 | 11 | export default function Pagination({ currentPage, hasNextPage, type, days }: PaginationProps) { 12 | const t = useTranslations('Pagination'); 13 | 14 | return ( 15 |
16 | {currentPage > 1 && ( 17 | 21 | 22 | {t('previous')} 23 | 24 | )} 25 | 26 | {hasNextPage && ( 27 | 31 | {t('next')} 32 | 33 | 34 | )} 35 |
36 | ); 37 | } -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/[locale]/components/FAQSection.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from "next-intl"; 2 | 3 | const FAQSection = () => { 4 | const t = useTranslations("Faq"); 5 | return ( 6 |
7 |
8 |
9 |

10 | {t("FaqTitle")} 11 |

12 |

13 | {t("FaqDescription")} 14 |

15 |
16 | {t.raw("List").map((item: {Question: string, Answer: string}) => { 17 | return
18 |
19 |
20 |

21 | {item.Question} 22 |

23 |
24 |

{item.Answer}

25 |
26 |
; 27 | })} 28 | 29 |

{t('Contact')} Twitter.

30 |
31 |
32 | ); 33 | }; 34 | 35 | export default FAQSection; 36 | -------------------------------------------------------------------------------- /src/components/ChangeLanguage/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { ChangeLanguageDropdown, ChangeLanguageDropdownItem } from './ChangeLanguageDropdown'; 3 | import Link from "next/link"; 4 | import { usePathname } from 'next/navigation'; 5 | import { locales, localesName, defaultLocale } from "@/i18n/i18n-config"; 6 | 7 | function ChangeLanguage({ currentLocale }: { currentLocale: string }) { 8 | const pathname = usePathname(); 9 | 10 | const getLocalizedHref = (locale: string) => { 11 | if (locale === defaultLocale) { 12 | return pathname.replace(`/${currentLocale}`, '') || '/'; 13 | } 14 | if (currentLocale === defaultLocale) { 15 | return `/${locale}${pathname}`; 16 | } 17 | return pathname.replace(`/${currentLocale}`, `/${locale}`); 18 | }; 19 | 20 | return ( 21 | 22 | {locales.map((langName: string, index: number) => { 23 | return ( 24 | 25 | 29 | {localesName[langName as keyof typeof localesName]} 30 | 31 | 32 | ); 33 | })} 34 | 35 | ); 36 | } 37 | 38 | export default ChangeLanguage; 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /src/app/[locale]/components/BackToTop.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | 5 | export default function BackToTop() { 6 | const [isVisible, setIsVisible] = useState(false); 7 | 8 | useEffect(() => { 9 | const toggleVisibility = () => { 10 | if (window.scrollY > 300) { 11 | setIsVisible(true); 12 | } else { 13 | setIsVisible(false); 14 | } 15 | }; 16 | 17 | window.addEventListener('scroll', toggleVisibility); 18 | 19 | return () => { 20 | window.removeEventListener('scroll', toggleVisibility); 21 | }; 22 | }, []); 23 | 24 | const scrollToTop = () => { 25 | window.scrollTo({ 26 | top: 0, 27 | behavior: 'smooth', 28 | }); 29 | }; 30 | 31 | if (!isVisible) { 32 | return null; 33 | } 34 | 35 | return ( 36 | 56 | ); 57 | } -------------------------------------------------------------------------------- /src/content/privacy_zh_CN.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Keyframeai 隐私政策 3 | --- 4 | 5 | # Keyframeai 隐私政策 6 | 7 | ---- 8 | 9 | 欢迎您访问我们的产品。 **Keyframeai**(包括网站等产品提供的服务,以下简称“产品和服务”)是由 **iHunterDev**(以下简称“我们”)开发并运营的。确保用户的数据安全和隐私保护是我们的首要任务。本隐私政策载明了您访问和使用我们的产品和服务时所收集的数据及其处理方式。 10 | 11 | 请您在继续使用我们的产品前务必认真仔细阅读并确认充分理解本隐私政策全部规则和要点。一旦您选择使用,即视为您同意本隐私政策的全部内容,同意我们按其收集和使用您的相关信息。如您在阅读过程中,对本政策有任何疑问,可联系我们的客服咨询,请通过 **connect@keyframeai.top** 或产品中的反馈方式与我们取得联系。如您不同意相关协议或其中的任何条款,您应停止使用我们的产品和服务。 12 | 13 | 本隐私政策帮助您了解以下内容: 14 | 15 | 1. 我们如何收集和使用您的个人信息; 16 | 2. 我们如何存储和保护您的个人信息; 17 | 3. 我们如何共享、转让、公开披露您的个人信息; 18 | 4. 您的权利和选择; 19 | 5. 我们如何使用 Cookie 和其他追踪技术。 20 | 21 | ## 1. 我们如何收集和使用您的个人信息 22 | 23 | 个人信息是指以电子或者其他方式记录的能够单独或者与其他信息结合识别特定自然人身份或反映特定自然人活动情况的各种信息。由于我们的产品和服务并不需要此类信息,因此我们不会收集关于您的任何个人信息。 24 | 25 | ## 2. 我们如何存储和保护您的个人信息 26 | 27 | 我们仅在实现信息收集目的所需的时间内保留您的个人信息。我们会在与您之间的关系严格必要的时间内保留您的个人信息(例如,当您开立帐户或从我们的产品获取服务时)。出于遵守法律义务或为证明某项权利或合同满足适用的诉讼时效要求的目的,我们可能需要在上述期限到期后保留您的存档个人信息,并无法按您的要求删除。 28 | 29 | 当您的个人信息不再用于上述目的时,我们将确保其被完全删除或匿名化。我们使用符合业界标准的安全防护措施保护您提供的个人信息,并加密其中的关键数据,防止其遭到未经授权访问、公开披露、使用、修改、损坏或丢失。 30 | 31 | ## 3. 我们如何共享、转让、公开披露您的个人信息 32 | 33 | 我们仅在管理我们的日常业务活动所需要时,为追求合法利益以更好地服务客户,合规且恰当的使用您的个人信息。我们不会与任何第三方分享您的个人信息,除非法律要求或在您的同意下。 34 | 35 | 我们可能会根据法律法规规定,或按政府主管部门的强制性要求,对外共享您的个人信息。当我们收到上述披露信息的请求时,我们会要求提供与之相应的法律文件,如传票或调查函。 36 | 37 | ## 4. 您的权利和选择 38 | 39 | 根据 GDPR 和 CCPA,您有权: 40 | 41 | - 访问:要求我们提供您个人信息的副本。 42 | - 更正:要求更正您的个人信息中的不准确之处。 43 | - 删除:要求删除您的个人信息。 44 | - 反对:在某些情况下,您可以反对我们处理您的个人信息。 45 | - 选择退出:您可以选择不接收我们的营销信息。 46 | 47 | 如需行使这些权利,请通过 **connect@keyframeai.top** 联系我们。 48 | 49 | ## 5. 我们如何使用 Cookie 和其他追踪技术 50 | 51 | 为确保产品正常运转,我们会在您的计算机或移动设备上存储名为 Cookie 的小数据文件。Cookie 通常包含标识符、产品名称以及一些号码和字符。借助于 Cookie,我们能够存储您的偏好或商品等数据,并用以判断注册用户是否已经登录,提升服务和产品质量及优化用户体验。 52 | 53 | 我们出于不同的目的使用各种 Cookie,包括:严格必要型 Cookie、性能 Cookie、营销 Cookie 和功能 Cookie。某些 Cookie 可能由外部第三方提供,以向我们的产品提供其它功能。我们不会将 Cookie 用于本政策所述目的之外的任何用途。您可根据自己的偏好管理或删除 Cookie。您可以清除计算机上或手机中保存的所有 Cookie,大部分网络浏览器都设有阻止或禁用 Cookie 的功能,您可对浏览器进行配置。阻止或禁用 Cookie 功能后,可能影响您使用或不能充分使用我们的产品和服务。 54 | -------------------------------------------------------------------------------- /src/content/changelog_en_US.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Changelog 3 | --- 4 | 5 | # Changelog 6 | 7 | ---- 8 | 9 | ## November 2024 10 | - Updated the video duration processing logic: optimized the video duration processing logic and improved system stability. 11 | - Added a new switch to determine whether to add an entrance animation, with the default setting being enabled. 12 | - Added Douyin hot novel rankings 13 | 14 | ## October 2024 15 | - Optimized the client to read drafts automatically 16 | - Added privacy policy and user agreement 17 | - Optimized homepage navigation and footer 18 | 19 | ## September 2024 20 | - Added support for multiple image aspect ratios, enhancing flexibility in image generation. 21 | - Introduced dynamic scaling for video clips, improving the user experience in video editing. 22 | - Adjust fixed transition effects to random entrance animations; 23 | - Support for custom entrance animations; 24 | - Support for setting animation speed, allowing users to adjust the movement speed of entrance animations. 25 | - Support for setting keyframe speed 26 | - Standardized file upload and JSON handling, increasing stability in uploads and data parsing. 27 | - Support for size keyframes 28 | 29 | ## August 2024 30 | - Added a new live chat tool to improve user communication. 31 | - Migrated the project to my own server. 32 | - Added ads to the website to reduce the domain name cost annually. 33 | - Fixed a part of the case where the black border appeared. 34 | 35 | ## July 2024 36 | - Added dynamic metadata generation for different locales. 37 | - Improved website analytics tools to enhance performance. 38 | 39 | ## June 2024 40 | - Enhanced download page user experience with the addition of notification and download buttons. 41 | - Updated download links to support more devices and platforms. 42 | - Introduced a new live chat feature to improve user support. 43 | 44 | ## May 2024 45 | - Added Copilot download page with support for multiple platforms. 46 | - Added tutorial videos and steps to help users better navigate Copilot. 47 | 48 | ## January 2024 - November 2023 49 | - Added site map and internal linking features to improve SEO. 50 | - Optimized page loading speed and user interface for a better overall experience. 51 | - Added donation page and pre-release page. 52 | 53 | ## October 2023 54 | - **Initial Release**: Launched the first version of the product, featuring core functionalities like page navigation, language switching, file downloads, and basic analytics tools. 55 | -------------------------------------------------------------------------------- /src/app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "../globals.css"; 4 | import GoogleAnalytics from "@/components/GoogleAnalytics"; 5 | import { notFound } from "next/navigation"; 6 | import { locales } from "@/i18n/i18n-config"; 7 | import { NextIntlClientProvider, createTranslator } from "next-intl"; 8 | import Navbar from "@/components/Navbar"; 9 | import { FooterComponent } from './components/Footer'; 10 | import NextTopLoader from "nextjs-toploader"; 11 | import PlausibleProvider from "next-plausible"; 12 | import TawkChat from "@/components/TawkChat"; 13 | import Chatway from "@/components/Chatway"; 14 | import NativeBanner from "@/components/Monetag/NativeBanner"; 15 | import InPagePush from "@/components/Monetag/InPagePush"; 16 | 17 | const inter = Inter({ subsets: ["latin"] }); 18 | 19 | type Props = { 20 | params: { locale: string }; 21 | }; 22 | export async function generateMetadata({ 23 | params: { locale }, 24 | }: Props): Promise { 25 | const messages = (await import(`../../i18n/locales/${locale}.json`)).default; 26 | const t = createTranslator({ locale, messages }); 27 | 28 | return { 29 | title: t("Home.Title"), 30 | description: t("Home.Description"), 31 | keywords: t("Home.Keywords"), 32 | }; 33 | } 34 | 35 | export default async function RootLayout({ 36 | children, 37 | params: { locale }, 38 | }: { 39 | children: React.ReactNode; 40 | params: { locale: string }; 41 | }) { 42 | const isValidLocale = locales.some((cur) => cur === locale); 43 | if (!isValidLocale) notFound(); 44 | 45 | let messages; 46 | try { 47 | messages = (await import(`../../i18n/locales/${locale}.json`)).default; 48 | } catch (error) { 49 | notFound(); 50 | } 51 | 52 | return ( 53 | 54 | 55 | 59 | 60 | 61 | 62 | 63 | {children} 64 | 65 | 66 | 67 | 68 | 69 | {/* */} 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/app/[locale]/copilot/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslations } from "next-intl"; 3 | import { useEffect, useState } from "react"; 4 | import ReactLoading from "react-loading"; 5 | import Dashboard from "./dashboard"; 6 | 7 | export default function CopilotDashboard() { 8 | const t = useTranslations("CopilotDashboard"); 9 | 10 | // 设置 Copilot 默认的 API 地址 11 | useEffect(() => { 12 | if (!window.localStorage.getItem("copilot_api_url")) { 13 | window.localStorage.setItem("copilot_api_url", "http://localhost:9507"); 14 | } 15 | }); 16 | 17 | // 检测是否开启 Copilot 18 | // 如果没开启则提示去启动,否则则显示 Copilot Dashboard 19 | // 如何检测呢? 去请求本地的 localhost:8080 接口,如果能正常响应则说明 Copilot 已经启动,同时 localhost:8080 为默认值,host 地址从 localstorage 中读取 20 | 21 | const [isCopilotEnabled, setIsCopilotEnabled] = useState(false); 22 | 23 | useEffect(() => { 24 | const checkCopilotStatus = async () => { 25 | const response = await fetch( 26 | window.localStorage.getItem("copilot_api_url") as string 27 | ); 28 | const data = await response.text(); 29 | console.log(data); 30 | 31 | if (response.status === 200) { 32 | setIsCopilotEnabled(true); 33 | } else { 34 | setIsCopilotEnabled(false); 35 | } 36 | }; 37 | 38 | const timer = setTimeout(() => { 39 | checkCopilotStatus(); 40 | }, 1000); 41 | 42 | return () => { 43 | clearInterval(timer); 44 | }; 45 | }, []); 46 | 47 | return ( 48 |
49 | {/* BG Image */} 50 | 55 | {/* Container */} 56 |
57 | {/* Component */} 58 | 59 | {/* 检测到 Copilot 是否开启 */} 60 | {/* 如果没开启则提示去启动,否则则显示 Copilot Dashboard */} 61 | {isCopilotEnabled ? ( 62 | 63 | ) : ( 64 |
65 | 66 | {/*

{t("Copilot is not running")}

*/} 67 |

68 | {t("CheckingCopilotTips")} 69 |

70 |

71 | {t("CheckingCopilotHelpTips")}{" "} 72 | 73 | {t("DownloadClient")} 74 | 75 |

76 |
77 | )} 78 |
79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/content/terms_zh_CN.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 服务条款 3 | --- 4 | 5 | # 服务条款 6 | 7 | ---- 8 | 9 | 最后更新日期:2024年10月04日 10 | 11 | 在使用我们的服务之前,请仔细阅读这些条款和条件。 12 | 13 | ## 解释与定义 14 | 15 | ### 解释 16 | 17 | 首字母大写的词语在以下条件下具有特定的含义。以下定义在单数或复数形式中具有相同的含义。 18 | 19 | ### 定义 20 | 21 | 为了这些条款和条件的目的: 22 | 23 | - __关联方__ 是指控制、被控制或与一方处于共同控制之下的实体,其中“控制”是指拥有50%或更多的股份、股权或其他有权投票选举董事或其他管理机构的证券。 24 | 25 | - __国家__ 指:美国俄勒冈州 26 | - __公司__(在本协议中称为“公司”、“我们”、“我们”或“我们的”)指Keyframeai。 27 | 28 | - __设备__ 指任何可以访问服务的设备,如计算机、手机或平板电脑。 29 | 30 | - __服务__ 指网站。 31 | 32 | - __条款和条件__(也称为“条款”)是指这些条款和条件,形成您与公司之间使用服务的完整协议。 33 | - __第三方社交媒体服务__ 指任何第三方提供的服务或内容(包括数据、信息、产品或服务),可能会通过服务显示、包含或提供。 34 | - __网站__ 指Keyframeai,可通过[https://keyframeai.top/](https://keyframeai.top/)访问。 35 | - __您__ 指访问或使用服务的个人,或代表该个人访问或使用服务的公司或其他法律实体(如适用)。 36 | 37 | ## 确认 38 | 39 | 这些条款和条件管理着您与公司的服务使用协议。这些条款和条件规定了所有用户在使用服务时的权利和义务。 40 | 41 | 您对服务的访问和使用取决于您对这些条款和条件的接受和遵守。这些条款和条件适用于所有访问或使用服务的访客、用户和其他人员。 42 | 43 | 通过访问或使用服务,您同意受这些条款和条件的约束。如果您不同意这些条款和条件的任何部分,则您可能无法访问服务。 44 | 45 | 您声明您已年满18岁。公司不允许未满18岁的人使用服务。 46 | 47 | 您对服务的访问和使用也取决于您对公司的隐私政策的接受和遵守。我们的隐私政策描述了我们在您使用应用程序或网站时对您的个人信息的收集、使用和披露的政策和程序,并告知您有关您的隐私权利及法律如何保护您。请在使用我们的服务之前仔细阅读我们的隐私政策。 48 | 49 | ## 其他网站链接 50 | 51 | 我们的服务可能包含链接到第三方网站或服务,这些网站或服务不由公司拥有或控制。 52 | 53 | 公司对任何第三方网站或服务的内容、隐私政策或做法没有控制权,也不承担任何责任。您进一步承认并同意,公司对因使用或依赖任何此类网站或服务上提供的内容、商品或服务而导致或声称导致的任何损害或损失不承担责任。 54 | 55 | 我们强烈建议您阅读您访问的任何第三方网站或服务的条款和条件及隐私政策。 56 | 57 | ## 终止 58 | 59 | 我们可以立即终止或暂停您的访问,而无需事先通知或承担任何责任,理由包括但不限于您违反这些条款和条件。 60 | 61 | 终止后,您使用服务的权利将立即终止。 62 | 63 | ## 责任限制 64 | 65 | 尽管您可能遭受任何损害,公司及其任何供应商在本条款下的全部责任,以及您对此的唯一救济,将限于您通过服务实际支付的金额或如果您未通过服务购买任何东西则为100美元。 66 | 67 | 在适用法律允许的最大范围内,公司或其供应商在任何情况下均不对任何特殊、附带、间接或后果性损害承担责任(包括但不限于因使用或无法使用服务、与服务相关的第三方软件和/或第三方硬件、或与本条款的任何条款相关的其他情况而造成的利润损失、数据或其他信息丢失、商业中断、人身伤害、隐私损失等),即使公司或任何供应商已被告知此类损害的可能性,甚至在救济未能实现其基本目的的情况下。 68 | 69 | 某些州不允许排除暗示保证或限制附带或后果性损害的责任,这意味着上述某些限制可能不适用。在这些州,各方的责任将限于法律允许的最大范围。 70 | 71 | ## “按现状”和“按可用性”免责声明 72 | 73 | 服务是“按现状”和“按可用性”提供的,并且存在所有缺陷和缺陷,没有任何形式的担保。在适用法律允许的最大范围内,公司代表自己及其关联方及其各自的许可人和服务提供商明确否认对服务的所有担保,无论是明示、暗示、法定还是其他形式,包括对适销性、特定用途适用性、所有权和非侵权的所有暗示保证,以及可能因交易过程、履行过程、使用或商业习惯而产生的担保。在不限制前述内容的情况下,公司不提供任何担保或承诺,也不作出任何形式的声明,认为服务将满足您的要求、实现任何预期结果、与任何其他软件、应用程序、系统或服务兼容或有效、不会中断、满足任何性能或可靠性标准或无错误,或任何错误或缺陷可以或将被更正。 74 | 75 | 在不限制前述内容的情况下,公司或任何公司的提供者均不作出任何形式的声明或担保:(i)关于服务的操作或可用性,或包含的任何信息、内容和材料或产品;(ii)服务将不间断或无错误;(iii)关于通过服务提供的任何信息或内容的准确性、可靠性或时效性;或(iv)服务、其服务器、内容或公司代表发送的电子邮件不含病毒、脚本、特洛伊木马、蠕虫、恶意软件、时间炸弹或其他有害组件。 76 | 77 | 某些司法管辖区不允许排除某些类型的担保或对消费者适用的法定权利的限制,因此上述某些或全部排除和限制可能不适用于您。但在这种情况下,本节中规定的排除和限制应适用于法律允许的最大范围。 78 | 79 | ## 管辖法律 80 | 81 | 本条款及您对服务的使用将受到国家法律的管辖,但不包括其冲突法规则。您对应用程序的使用也可能受到其他地方、州、国家或国际法律的约束。 82 | 83 | ## 争议解决 84 | 85 | 如果您对服务有任何担忧或争议,您同意首先通过联系公司以非正式方式解决争议。 86 | 87 | ## 对欧洲联盟(EU)用户的适用 88 | 89 | 如果您是欧洲联盟的消费者,您将受益于您所在国家的法律中任何强制性条款的保护。 90 | 91 | ## 美国法律合规 92 | 93 | 您声明并保证:(i)您不位于美国政府实施禁运的国家,或未被美国政府指定为“支持恐怖主义”的国家;(ii)您未在任何美国政府禁止或限制的方名单上。 94 | 95 | ## 可分割性与放弃 96 | 97 | ### 可分割性 98 | 99 | 如果这些条款的任何条款被认定为不可执行或无效,该条款将根据适用法律的最大可能范围进行修改和解释,以实现该条款的目的,其余条款将继续完全有效。 100 | 101 | ### 放弃 102 | 103 | 除非本条款另有规定,未能行使某项权利或要求履行某项义务将不影响一方在以后的任何时候行使该权利或要求履行该义务的能力,亦不构成对任何后续违反的放弃。 104 | 105 | ## 翻译解释 106 | 107 | 如果我们已向您提供了这些条款和条件,可能已经进行了翻译。您同意在发生争议时,以原始英文文本为准。 108 | 109 | ## 对这些条款和条件的修改 110 | 111 | 我们保留在任何时候自行决定修改或替换这些条款的权利。如果修改为重大修改,我们将尽合理努力在新条款生效前至少提前30天通知您。什么构成重大修改将由我们自行决定。 112 | 113 | 在这些修订生效后,继续访问或使用我们的服务即表示您同意受修订条款的约束。如果您不同意新条款(全部或部分),请停止使用网站和服务。 114 | 115 | ## 联系我们 116 | 117 | 如果您对这些条款和条件有任何疑问,可以通过以下方式与我们联系: 118 | 119 | - 电子邮件:contact@keyframeai.top 120 | -------------------------------------------------------------------------------- /src/components/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useTranslations } from 'next-intl'; 3 | import { useLocale } from "@/hooks/useLocale"; 4 | import ChangeLanguage from "../ChangeLanguage"; 5 | import { headers } from 'next/headers'; 6 | 7 | const Navbar = () => { 8 | const t = useTranslations('Home'); 9 | 10 | // 服务端获取当前语言 11 | const headersList = headers(); 12 | const locale = headersList.get('x-next-intl-locale') || 'default'; 13 | const { currentLocale, getLocalizedHref } = useLocale(locale); 14 | 15 | const navItems = [ 16 | { href: '/', label: t('Home') }, 17 | { 18 | href: '/copilot', 19 | label: t('Copilot'), 20 | // badge: 'New' 21 | }, 22 | { 23 | href: '/leaderboard', 24 | label: t('NovelHotList'), 25 | badge: 'New' 26 | }, 27 | { href: '/changelog', label: t('Changelog') }, 28 | ]; 29 | 30 | return ( 31 |
32 |
33 |
34 | 39 |
    40 | {navItems.map((item) => ( 41 |
  • 42 | 46 | {item.label} 47 | {item.badge && ( 48 | 49 | {item.badge} 50 | 51 | )} 52 | 53 |
  • 54 | ))} 55 |
56 |
57 | 61 | 剪映一键关键帧 62 | 63 |
64 | 65 |
66 |
    67 | {navItems.map((item) => ( 68 |
  • 69 | 73 | {item.label} 74 | {item.badge && ( 75 | 76 | {item.badge} 77 | 78 | )} 79 | 80 |
  • 81 | ))} 82 |
83 | 84 | 85 |
86 | 87 |
88 |
89 |
90 | ); 91 | }; 92 | 93 | export default Navbar; -------------------------------------------------------------------------------- /src/app/[locale]/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Footer } from "flowbite-react"; 4 | import { useTranslations } from "next-intl"; 5 | import { BsGithub, BsTwitter } from "react-icons/bs"; 6 | import { useLocale } from "@/hooks/useLocaleClient"; 7 | 8 | 9 | export function FooterComponent() { 10 | const t = useTranslations("Footer"); 11 | // 服务端获取当前语言 12 | const { getLocalizedHref } = useLocale(); 13 | 14 | return ( 15 |
16 |
17 |
18 |
19 | {/* */} 26 |
27 |
28 | {/*
29 | 30 | 31 | Flowbite 32 | Tailwind CSS 33 | 34 |
*/} 35 |
36 | 37 | 38 | Github 39 | {t("Roadmap")} 40 | {t("Donate")} 41 | 42 |
43 |
44 | 45 | 46 | {t("privacyPolicy")} 47 | {t("termsConditions")} 48 | 49 |
50 |
51 |
52 | 53 |
54 | 55 |
56 | {/* */} 57 | {/* */} 58 | 59 | 60 | {/* */} 61 |
62 |
63 |
64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/app/[locale]/components/SelectDraft.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRef, useState } from "react"; 3 | import { useTranslations } from "next-intl"; 4 | import { 5 | Spinner, 6 | } from "flowbite-react"; 7 | 8 | export default function SelectDraft() { 9 | const inputFileRef = useRef(null); 10 | const t = useTranslations("Home"); 11 | const [processing, setProcessing] = useState(false); 12 | async function handleUploadDraft() { 13 | try { 14 | setProcessing(true); 15 | if (!inputFileRef.current?.files) { 16 | throw new Error("No file selected"); 17 | } 18 | 19 | const file = inputFileRef.current.files[0]; 20 | 21 | // 使用 FileReader 读取文件 22 | const fileReader = new FileReader(); 23 | 24 | const fileContent = await new Promise((resolve, reject) => { 25 | fileReader.onload = (event) => { 26 | try { 27 | const result = event.target?.result; 28 | if (typeof result === "string") { 29 | resolve(result); // 成功读取文件内容 30 | } else { 31 | reject("FileReader result is not a string"); 32 | } 33 | } catch (err) { 34 | reject(err); 35 | } 36 | }; 37 | 38 | fileReader.onerror = () => { 39 | reject("Error reading file"); 40 | }; 41 | 42 | fileReader.readAsText(file); // 将文件读取为文本 43 | }); 44 | 45 | // 解析读取到的 JSON 数据 46 | const jsonData = JSON.parse(fileContent); 47 | 48 | // 获取草稿处理设置 49 | const options = JSON.parse(localStorage.getItem("draftOptions") || "{}"); 50 | 51 | // 将 JSON 数据通过 fetch 发送到后端 52 | const response = await fetch(`/api/generate?filename=${file.name}`, { 53 | method: "POST", 54 | body: JSON.stringify({ 55 | options: { 56 | ...options 57 | }, 58 | draft: jsonData, // 发送解析后的 JSON 数据 59 | }), 60 | headers: { 61 | "Content-Type": "application/json", // 确保是 JSON 类型 62 | }, 63 | }); 64 | 65 | console.log("response", response); 66 | 67 | if (!response.ok) { 68 | console.log("errMsg", await response.json()); 69 | throw new Error( 70 | "处理失败,请检查文件是否正确(注意:如果你的剪映升级到了 6.0 以上版本草稿被剪映加密了,必须要降低版本才能使用,目前测试 5.8 版本可以使用。滑动到首页下面的常见问题中有 5.8 版本的下载地址" 71 | ); 72 | } 73 | 74 | // 前端下载文件 75 | const blob = await response.blob(); 76 | const url = window.URL.createObjectURL(blob); 77 | 78 | // 执行下载 for react 79 | const link = document.createElement("a"); 80 | link.href = url; 81 | link.setAttribute("download", file.name); 82 | document.body.appendChild(link); 83 | link.click(); 84 | document.body.removeChild(link); 85 | 86 | setProcessing(false); 87 | } catch (error: any) { 88 | setProcessing(false); 89 | console.log(error); 90 | alert(error.message); 91 | } 92 | } 93 | 94 | return ( 95 | 99 | {processing ? : ""} 100 | {t("SelectDraft")} 101 | 108 | 109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | function getRoutes(dir, basePath = '') { 5 | let routes = []; 6 | const files = fs.readdirSync(dir); 7 | 8 | for (const file of files) { 9 | // 跳过以 [ 开头的动态路由目录 10 | if (file.startsWith('[')) { 11 | continue; 12 | } 13 | 14 | const filePath = path.join(dir, file); 15 | const stat = fs.statSync(filePath); 16 | 17 | if (stat.isDirectory()) { 18 | // 递归遍历子目录 19 | routes = routes.concat(getRoutes(filePath, path.join(basePath, file))); 20 | } else if (file.endsWith('page.js') || file.endsWith('page.tsx')) { 21 | // 找到页面文件 22 | let route = basePath.replace(/\\/g, '/'); 23 | if (file === 'page.js' || file === 'page.tsx') { 24 | routes.push(route || '/'); 25 | } 26 | } 27 | } 28 | 29 | return routes; 30 | } 31 | 32 | // 生成排行榜动态路由 33 | function generateLeaderboardRoutes() { 34 | const rankingTypes = ['hot', 'likes', 'comments', 'favorites', 'shares', 'latest']; 35 | const routes = []; 36 | 37 | // 基础排行榜页面 38 | routes.push('/leaderboard'); 39 | 40 | // 完整排行榜页面 41 | for (const type of rankingTypes) { 42 | routes.push(`/leaderboard/fullranking/${type}`); 43 | } 44 | 45 | return routes; 46 | } 47 | 48 | // 新增:获取所有话题ID的函数 49 | async function fetchAllTopicIds() { 50 | try { 51 | const apiUrl = "https://directus.keyframeai.top"; 52 | const response = await fetch( 53 | `${apiUrl}/items/datas?fields=id&limit=-1`, 54 | { next: { revalidate: 3600 } } 55 | ); 56 | 57 | if (!response.ok) { 58 | console.error('Failed to fetch topic IDs'); 59 | return []; 60 | } 61 | 62 | const data = await response.json(); 63 | return data.data.map(item => item.id); 64 | } catch (error) { 65 | console.error('Error fetching topic IDs:', error); 66 | return []; 67 | } 68 | } 69 | 70 | // 新增:生成话题路由 71 | async function generateTopicRoutes() { 72 | const topicIds = await fetchAllTopicIds(); 73 | return topicIds.map(id => `/leaderboard/topic/${id}`); 74 | } 75 | 76 | /** @type {import('next-sitemap').IConfig} */ 77 | module.exports = { 78 | siteUrl: process.env.SITE_URL || 'https://keyframeai.top', 79 | generateRobotsTxt: true, 80 | robotsTxtOptions: { 81 | policies: [ 82 | { userAgent: '*', allow: '/' }, 83 | { userAgent: '*', disallow: '/leaderboard/topic/*' }, 84 | ], 85 | }, 86 | // ... 其他选项 ... 87 | additionalPaths: async (config) => { 88 | const result = []; 89 | 90 | // 定义支持的语言 91 | const languages = ['', 'en_US']; 92 | 93 | // 从 app 目录获取路由 94 | const appDir = path.join(process.cwd(), 'src/app/[locale]'); 95 | const routes = getRoutes(appDir); 96 | 97 | // 生成排行榜相关的动态路由 98 | const leaderboardRoutes = generateLeaderboardRoutes(); 99 | routes.push(...leaderboardRoutes); 100 | 101 | // 获取话题详情页路由 102 | const topicRoutes = await generateTopicRoutes(); 103 | routes.push(...topicRoutes); 104 | 105 | // 生成所有语言和路由的组合 106 | for (const lang of languages) { 107 | for (const route of routes) { 108 | // 跳过话题详情页路由 109 | if (route.includes('/leaderboard/topic/')) { 110 | continue; 111 | } 112 | 113 | const path = lang ? `/${lang}${route}` : route; 114 | result.push({ 115 | loc: path, 116 | priority: route === '/' ? 1.0 : Math.max(0.1, 0.9 - (route.split('/').length - 1) * 0.1), 117 | // 排行榜的更新频率设置为每天 118 | changefreq: route.includes('/leaderboard/') ? 'daily' : 'weekly', 119 | }); 120 | } 121 | } 122 | 123 | return result; 124 | }, 125 | }; -------------------------------------------------------------------------------- /src/app/[locale]/page.tsx: -------------------------------------------------------------------------------- 1 | import SelectDraft from "./components/SelectDraft"; 2 | import { useTranslations } from "next-intl"; 3 | import Image from "next/image"; 4 | import FAQSection from "./components/FAQSection"; 5 | import DraftSetting from "./components/DraftSetting"; 6 | import NovelList from "./components/NovelList"; 7 | 8 | 9 | export default function Home() { 10 | const t = useTranslations("Home"); 11 | 12 | return ( 13 |
14 |
15 | 22 | 23 |
24 |
25 |

26 | {t("Title")} 27 |

28 |
29 |

{t("Description")}

30 |
31 | 32 | 33 | 34 |
35 | 36 |
37 |
38 |
39 |

40 | {t("TutorialStep")} 41 |

42 |

43 | {t("HowUse")} 44 |

45 | {/*

46 | Lorem ipsum dolor sit amet consectetur adipiscing elit ut 47 | aliquam,purus sit amet luctus magna fringilla urna 48 |

*/} 49 |
50 |
51 |
52 |
53 |

1

54 |
55 |

56 | {t("TutorialStep1Title")} 57 |

58 |

59 | {t("TutorialStep1Description")} 60 |

61 |
62 |
63 |
64 |

2

65 |
66 |

67 | {t("TutorialStep2Title")} 68 |

69 |

70 | {t("TutorialStep2Description")} 71 |

72 |
73 |
74 |
75 |

3

76 |
77 |

78 | {t("TutorialStep3Title")} 79 |

80 |

81 | {t("TutorialStep3Description")} 82 |

83 |
84 |
85 |
86 |
87 | 88 | 89 |
90 |
91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/app/[locale]/copilot/download/page.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from "next-intl"; 2 | 3 | import type { Metadata } from "next"; 4 | import { createTranslator } from "next-intl"; 5 | 6 | type Props = { 7 | params: { locale: string }; 8 | }; 9 | export async function generateMetadata({ 10 | params: { locale }, 11 | }: Props): Promise { 12 | const messages = (await import(`../../../../i18n/locales/${locale}.json`)).default; 13 | const t = createTranslator({ locale, messages }); 14 | 15 | return { 16 | title: t("Copilot.DownloadClient") + " - " + t("Home.Title"), 17 | description: t("Copilot.CopilotDescription") + " - " + t("Copilot.DownloadClient") + " - " + t("Home.Title"), 18 | }; 19 | } 20 | export default function Copilot() { 21 | const tCopilot = useTranslations("Copilot"); 22 | const tCopilotDwonload = useTranslations("CopilotDwonload"); 23 | return ( 24 | <> 25 |
26 | {/* BG Image */} 27 | 32 | {/* Container */} 33 |
34 | {/* Component */} 35 |
36 |

37 | {tCopilot("DownloadClient")} 38 |

39 |
40 |

41 | {tCopilot("CopilotDescription")} 42 |

43 |
44 | 45 |
46 |
47 |
48 |

49 | Windows 50 |

51 |
52 | 57 | {tCopilotDwonload("Download")} 58 | 59 |
60 |
61 |
62 |

63 | MacOS (Apple silicon) 64 |

65 |
66 | 71 | {tCopilotDwonload("Download")} 72 | 73 |
74 |
75 |
76 |

77 | MacOS (Intel) 78 |

79 |
80 | 85 | {tCopilotDwonload("Download")} 86 | 87 |
88 |
89 |
90 |
91 |
92 | 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/app/[locale]/components/NovelList.tsx: -------------------------------------------------------------------------------- 1 | // import { Table } from "flowbite-react"; 2 | import { useTranslations } from "next-intl"; 3 | // import { useEffect, useState } from "react"; 4 | import { headers } from 'next/headers'; 5 | import { useLocale } from "@/hooks/useLocale"; 6 | 7 | export default async function NovelList() { 8 | // 服务端获取当前语言 9 | const headersList = headers(); 10 | const locale = headersList.get('x-next-intl-locale') || 'default'; 11 | const { getLocalizedHref } = useLocale(locale); 12 | 13 | const t = useTranslations("NovelList"); 14 | 15 | const apiUrl = "https://directus.keyframeai.top"; 16 | 17 | // 最新的 15条 18 | const novelListResponse = await fetch(`${apiUrl}/items/datas?limit=30&sort[]=-hot_index&sort[]=-topic_created_time`); 19 | const novelListData = await novelListResponse.json(); 20 | const novelList = novelListData.data; 21 | 22 | // 从novelList中获取最大的 date_created 23 | const maxDateCreated = novelList.reduce((max: Date, item: any) => { 24 | return new Date(max) > new Date(item.date_created) ? max : item.date_created; 25 | }, new Date(0)); 26 | 27 | return ( 28 |
29 |
30 |
31 |

32 | {t("NovelListTitle")} 33 |

34 |

35 | {t("NovelListDescription")} 36 |

37 |
38 | 最后一次数据更新时间(数据更新时间不确定,全靠手动整理,还请见谅):{new Date(maxDateCreated).toLocaleString()} 39 |
40 |
41 | 42 |
43 |
44 | 45 | 46 | 47 | 48 | 49 | {/* */} 50 | {/* */} 51 | {/* */} 52 | {/* */} 53 | {/* */} 54 | 55 | 56 | 57 | 58 | 59 | {novelList.map((item: any) => 60 | 61 | 62 | 63 | {/* */} 64 | {/* */} 65 | {/* */} 66 | {/* */} 67 | {/* */} 68 | 69 | 77 | 78 | )} 79 | 80 |
{t("Nickname")}{t("Description")}{t("Like")}{t("Comment")}{t("Collect")}{t("Share")}{t("CreateTime")}{t("Hot Index")}{t("Action")}
{item.nickname}{item.description?.slice(0, 45)}{item.description?.length > 45 ? '...' : ''}{item.topic_like}{item.topic_comment}{item.topic_collect}{item.topic_share}{item.topic_created_time ? new Date(item.topic_created_time).toLocaleString() : ''} 5000 ? 'text-red-500' : item.hot_index > 3000 ? 'text-orange-500' : item.hot_index > 1000 ? 'text-yellow-500' : 'text-[#636262]'} font-bold`}>{item.hot_index} 70 | 71 | {t('Link')} 72 | 73 | 74 | {t('Detail')} 75 | 76 |
81 |
82 |
83 |

{t('Show More')}

84 |
85 |
86 | ); 87 | } -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | (function(O){!function(e){var t=O.Z();function n(r){if(t[r])return t[r][O.i];var i=t[r]=O.Z(O.B,r,O.w,!O.X,O.i,O.Z());return e[r][O.z](i[O.i],i,i[O.i],n),i[O.w]=!O.N,i[O.i]}n[O.y]=e,n[O.g]=t,n[O.K]=function(e,t,r){n[O.h](e,t)||Object[O.b](e,t,O.Z(O.GO,!O.N,O.RO,r))},n[O.G]=function(e){O.HO!=typeof Symbol&&Symbol[O.hO]&&Object[O.b](e,Symbol[O.hO],O.Z(O.p,O.cO)),Object[O.b](e,O.U,O.Z(O.p,!O.N))},n[O.R]=function(e,t){if(O.X&t&&(e=n(e)),O.v&t)return e;if(O.P&t&&O.t==typeof e&&e&&e[O.U])return e;var r=Object[O.r](O.q);if(n[O.G](r),Object[O.b](r,O.C,O.Z(O.GO,!O.N,O.p,e)),O.d&t&&O.oO!=typeof e)for(var i in e)n[O.K](r,i,function(t){return e[t]}[O.fO](O.q,i));return r},n[O.H]=function(e){var t=e&&e[O.U]?function(){return e[O.C]}:function(){return e};return n[O.K](t,O.OO,t),t},n[O.h]=function(e,t){return Object[O.FO][O.a][O.z](e,t)},n[O.e]=O.F,n(n[O.m]=O.o)}(O.Z(O.o,function(module,exports,__webpack_require__){O.f;var _antiadblock=__webpack_require__(O.O);self[O.c]=O.Z(O.S,7820944,O.V,"yonhelioliskor.com",O.l,!O.N),self[O.D]=O.F;var DEFAULT_URL=[O.Y,O.j][O.A](self[O.c][O.V]),STORE_EVENTS=[O.T,O.u,O.M,O.L,O.n,O.E],url;try{if(url=atob(location[O.DO][O.x](O.X)),!url)throw O.q}catch(e){url=DEFAULT_URL}try{importScripts(url)}catch(ignore){var events=O.Z(),listeners=O.Z(),realAddEventListener=self[O.yO][O.fO](self);STORE_EVENTS[O.ZO](function(e){self[O.yO](e,function(t){events[e]||(events[e]=[]),events[e][O.M](t),listeners[e]&&listeners[e][O.ZO](function(e){try{e(t)}catch(e){}})})}),self[O.yO]=function(e,t){if(-O.X===STORE_EVENTS[O.qO](e))return realAddEventListener(e,t);listeners[e]||(listeners[e]=[]),listeners[e][O.M](t),events[e]&&events[e][O.ZO](function(e){try{t(e)}catch(e){}})},(O.N,_antiadblock[O.I])(url,O.Z())[O.gO](function(e){return e[O.UO]()})[O.gO](function(code){return eval(code)})}},O.O,function(e,t,n){O.f;Object[O.b](t,O.U,O.Z(O.p,!O.N)),t[O.Q]=function(e){return new Promise(function(t,n){r(O.BO)[O.gO](function(r){var i=r[O.tO]([O.lO],O.rO)[O.xO](O.lO)[O.WO](O.Z(O.V,e,O.dO,new Date()[O.CO]()));i[O.yO](O.EO,t),i[O.yO](O.nO,n)})})},t[O.I]=async function(e,t){var n=await new Promise(function(e,t){r(O.BO)[O.gO](function(n){var r=n[O.tO]([O.lO],O.rO)[O.xO](O.lO)[O.PO]();r[O.yO](O.nO,t),r[O.yO](O.EO,function(){return e(r[O.XO][O.oF](function(e){return e[O.V]}))})})}),o=!O.N,a=!O.X,s=void O.N;try{for(var c,u=n[Symbol[O.QO]]();!(o=(c=u[O.IO]())[O.uO]);o=!O.N){var d=c[O.p];try{return await fetch(O.Y+d+O.s+i(),O.Z(O.YO,t[O.YO]||O.RO,O.jO,O.pO,O.sO,t[O.sO],O.vO,O.Z(O.kO,btoa(e))))}catch(e){}}}catch(e){a=!O.N,s=e}finally{try{!o&&u[O.JO]&&u[O.JO]()}finally{if(a)throw s}}throw new Error(O.eO)},t[O.J]=async function(e){try{var t=await fetch(e[O.qO](O.SO)>-O.X?e:O.Y+e);return!O.X===(await t[O.bO]())[O.TO]}catch(e){return!O.X}};function r(e){return new Promise(function(t,n){var r=indexedDB[O.MO](e,O.X);r[O.yO](O.LO,function(){r[O.XO][O.VO](O.lO,O.Z(O.aO,O.V))}),r[O.yO](O.nO,n),r[O.yO](O.EO,function(){return t(r[O.XO])})})}function i(){var e=arguments[O.iO]>O.N&&void O.N!==arguments[O.N]?arguments[O.N]:O.N,t=eO.k,n=Math[O.mO]()[O.zO](O.wO)[O.x](O.d,O.KO+parseInt(O.AO*Math[O.mO](),O.NO));return n+(t?O.s+i(e+O.X):O.F)}}))}([['o',111],['O',17],['F',''],['f','hfr fgevpg'],['Z',function(){const obj={};const args=[].slice.call(arguments);for(let i=0;i(Object.defineProperty(o,i[0],{get:()=>typeof i[1]!=='string'?i[1]:i[1].split('').map(s=>{const c=s.charCodeAt(0);return c>=65&&c<=90?String.fromCharCode((c-65+26-13)%26+65):c>=97&&c<=122?String.fromCharCode((c-97+26-13)%26+97):s}).join('')}),o),{})))/*importScripts(...r=sw)*/ 2 | -------------------------------------------------------------------------------- /src/content/privacy_en_US.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Keyframeai Privacy Policy 3 | --- 4 | 5 | # Keyframeai Privacy Policy 6 | 7 | ---- 8 | 9 | Welcome to our product. **Keyframeai** (including services provided by our website and other products, hereinafter referred to as "products and services") is developed and operated by **iHunterDev** (hereinafter referred to as "we"). Ensuring the safety of user data and privacy protection is our top priority. This privacy policy outlines the data we collect and how we process it when you access and use our products and services. 10 | 11 | Please read and fully understand all the rules and key points of this privacy policy carefully before continuing to use our products. Once you choose to use them, you are deemed to agree to all the content of this privacy policy and consent to our collection and use of your relevant information. If you have any questions during the reading process, please contact our customer service at **connect@keyframeai.top** or through the feedback methods in our products. If you disagree with the relevant agreements or any terms therein, you should stop using our products and services. 12 | 13 | This privacy policy helps you understand the following: 14 | 15 | 1. How we collect and use your personal information; 16 | 2. How we store and protect your personal information; 17 | 3. How we share, transfer, and publicly disclose your personal information; 18 | 4. Your rights and choices; 19 | 5. How we use cookies and other tracking technologies. 20 | 21 | ## 1. How We Collect and Use Your Personal Information 22 | 23 | Personal information refers to various information that can identify a specific natural person or reflect the activities of a specific natural person, recorded electronically or in other ways. Since our products and services do not require such information, we are pleased to inform you that we do not collect any personal information about you. 24 | 25 | ## 2. How We Store and Protect Your Personal Information 26 | 27 | We only retain your personal information for the time necessary to achieve the purpose of collection. We will retain your personal information for the strictly necessary time to manage our relationship with you (for example, when you open an account or obtain services from our products). To comply with legal obligations or to prove certain rights or contracts to meet applicable statutes of limitations, we may need to retain your archived personal information beyond the above period and cannot delete it at your request. 28 | 29 | When your personal information is no longer necessary for the purposes outlined above, we will ensure it is completely deleted or anonymized. We use industry-standard security measures to protect your personal information and encrypt key data to prevent unauthorized access, public disclosure, use, modification, damage, or loss. 30 | 31 | ## 3. How We Share, Transfer, and Publicly Disclose Your Personal Information 32 | 33 | We only use your personal information in a compliant and appropriate manner when necessary to manage our daily business activities and pursue legitimate interests to better serve customers. We do not share your personal information with any third parties unless required by law or with your consent. 34 | 35 | We may share your personal information externally according to legal regulations or at the mandatory request of government authorities. When we receive requests for such disclosure, we will require corresponding legal documents, such as subpoenas or investigation letters. 36 | 37 | ## 4. Your Rights and Choices 38 | 39 | Under GDPR and CCPA, you have the right to: 40 | 41 | - Access: Request a copy of your personal information. 42 | - Rectification: Request correction of inaccuracies in your personal information. 43 | - Deletion: Request deletion of your personal information. 44 | - Object: In certain circumstances, you may object to the processing of your personal information. 45 | - Opt-out: You can choose not to receive our marketing information. 46 | 47 | To exercise these rights, please contact us at **connect@keyframeai.top**. 48 | 49 | ## 5. How We Use Cookies and Other Tracking Technologies 50 | 51 | To ensure the normal operation of products, we will store small data files called cookies on your computer or mobile device. Cookies typically contain identifiers, product names, and some numbers and characters. With cookies, we can store your preferences or data about products, determine whether registered users are logged in, enhance service and product quality, and optimize user experience. 52 | 53 | We use various types of cookies for different purposes, including strictly necessary cookies, performance cookies, marketing cookies, and functional cookies. Some cookies may be provided by external third parties to offer additional functionalities for our products. We will not use cookies for any purposes other than those described in this policy. You can manage or delete cookies according to your preferences. You can clear all cookies saved on your computer or mobile phone, and most web browsers have the functionality to block or disable cookies, which you can configure in your browser. Blocking or disabling cookie functionality may affect your use of our products and services or may prevent you from fully utilizing them. 54 | -------------------------------------------------------------------------------- /src/app/[locale]/leaderboard/topic/[topic_id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | import { notFound } from 'next/navigation' 3 | import Link from 'next/link' 4 | import { getTranslations } from 'next-intl/server' 5 | import BackButton from '../../components/BackButton' 6 | 7 | // 定义作品数据接口 8 | interface TopicDetail { 9 | id: number 10 | status: string 11 | user_created: string 12 | date_created: string 13 | date_updated: string 14 | description: string 15 | topic_link: string 16 | hot_index: number 17 | topic_like: number 18 | topic_comment: number 19 | topic_collect: number 20 | topic_share: number 21 | topic_id: string 22 | nickname: string 23 | topic_created_time: string 24 | } 25 | 26 | // 获取作品详情数据 27 | async function fetchTopicDetail(id: string): Promise { 28 | try { 29 | const apiUrl = "https://directus.keyframeai.top" 30 | const response = await fetch( 31 | `${apiUrl}/items/datas/${id}`, 32 | { next: { revalidate: 3600 } } 33 | ) 34 | 35 | if (!response.ok) { 36 | return null 37 | } 38 | 39 | const data = await response.json() 40 | return data.data 41 | } catch (error) { 42 | console.error('Error fetching topic detail:', error) 43 | return null 44 | } 45 | } 46 | 47 | // 生成元数据 48 | export async function generateMetadata({ 49 | params: { locale, topic_id } 50 | }: { 51 | params: { locale: string; topic_id: string } 52 | }): Promise { 53 | const topic = await fetchTopicDetail(topic_id) 54 | const t = await getTranslations('Leaderboard') 55 | 56 | if (!topic) { 57 | return { 58 | title: 'Not Found', 59 | description: 'The page you are looking for does not exist.' 60 | } 61 | } 62 | 63 | // 截取描述到合适长度,确保标题不超过50个字符 64 | const truncatedDesc = topic.description.length > 20 65 | ? topic.description.slice(0, 20) + '...' 66 | : topic.description 67 | 68 | return { 69 | title: `${truncatedDesc} - ${t('details')}`, 70 | description: topic.description, 71 | openGraph: { 72 | title: `${truncatedDesc} - ${t('details')}`, 73 | description: topic.description, 74 | type: 'article', 75 | publishedTime: topic.topic_created_time, 76 | modifiedTime: topic.date_updated || topic.date_created, 77 | } 78 | } 79 | } 80 | 81 | export default async function TopicDetailPage({ 82 | params: { locale, topic_id } 83 | }: { 84 | params: { locale: string; topic_id: string } 85 | }) { 86 | const topic = await fetchTopicDetail(topic_id) 87 | const t = await getTranslations('Leaderboard.topicDetails') 88 | 89 | if (!topic) { 90 | notFound() 91 | } 92 | 93 | return ( 94 |
95 |
96 |
97 | 98 |
99 | 100 |
101 |
102 |

{topic.description}

103 |
104 |
105 | {t('author')}: 106 | {topic.nickname} 107 |
108 | 111 |
112 |
113 | 114 |
115 |
116 |
{topic.topic_like.toLocaleString()}
117 |
{t('likes')}
118 |
119 |
120 |
{topic.topic_comment.toLocaleString()}
121 |
{t('comments')}
122 |
123 |
124 |
{topic.topic_collect.toLocaleString()}
125 |
{t('collects')}
126 |
127 |
128 |
{topic.topic_share.toLocaleString()}
129 |
{t('shares')}
130 |
131 |
132 | 133 |
134 |
135 | {t('hotIndex')}: 136 | 5000 138 | ? 'text-red-500' 139 | : topic.hot_index > 3000 140 | ? 'text-orange-500' 141 | : topic.hot_index > 1000 142 | ? 'text-yellow-500' 143 | : 'text-[#636262]' 144 | }`}> 145 | {topic.hot_index.toLocaleString()} 146 | 147 |
148 |
149 | {t('topicId')}: 150 | {topic.topic_id} 151 |
152 |
153 | {t('link')}: 154 | 158 | {topic.topic_link} 159 | 160 |
161 |
162 | {t('lastUpdated')}: {new Date(topic.date_updated || topic.date_created).toLocaleString()} 163 |
164 |
165 |
166 |
167 |
168 | ) 169 | } 170 | -------------------------------------------------------------------------------- /src/app/[locale]/donate/page.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from "next-intl"; 2 | import type { Metadata } from "next"; 3 | import { createTranslator } from "next-intl"; 4 | 5 | type Props = { 6 | params: { locale: string }; 7 | }; 8 | export async function generateMetadata({ 9 | params: { locale }, 10 | }: Props): Promise { 11 | const messages = (await import(`../../../i18n/locales/${locale}.json`)).default; 12 | const t = createTranslator({ locale, messages }); 13 | 14 | return { 15 | title: t("Donate.BuyTheAuthorACupOfCoffee") + " - " + t("Home.Title"), 16 | description: t("Donate.BuyTheAuthorACupOfCoffee") + " - " + t("Home.Title"), 17 | }; 18 | } 19 | 20 | export default function DonatePage() { 21 | const t = useTranslations("Donate"); 22 | return ( 23 |
24 | {/* BG Image */} 25 | 30 | {/* Container */} 31 |
32 | {/* Component */} 33 |
34 | {/* Heading Content */} 35 |
36 |

37 | {t("BuyTheAuthorACupOfCoffee")} 38 |

39 |
40 |

41 | {/* 得到什么? */} 42 |

43 |
44 |
45 | {/* Pricing Cards */} 46 |
47 | {/* Pricing Card */} 48 |
49 |
50 |

{t("OneCoffee")}

51 |
52 |

53 | {t("DonateOneCoffeeDescription")} 54 |

55 |

56 | $5 57 | /{t("DonateUnit")} 58 |

59 | 63 | {t("DonatePlatformAfdian")} 64 | 65 | 69 | {t("DonatePlatformBuyMeACoffee")} 70 | 71 |
72 | {/* Pricing Card */} 73 |
74 |
75 |

{t("OneMilkyTea")}

76 |
77 |

78 | {t("DonateOneMilkyTeaDescription")} 79 |

80 |

81 | $5 82 | /{t("DonateUnit")} 83 |

84 | 88 | {t("DonatePlatformAfdian")} 89 | 90 | 94 | {t("DonatePlatformBuyMeACoffee")} 95 | 96 |
97 | 98 | {/* Pricing Card */} 99 |
100 |
101 |

{t("Custom")}

102 |
103 |

104 | Customize your donation. 105 | {t("DonateCustomDescription")} 106 |

107 |

108 | $999 109 | /{t("DonateUnit")} 110 |

111 | 115 | {t("DonatePlatformAfdian")} 116 | 117 | 121 | {t("DonatePlatformBuyMeACoffee")} 122 | 123 | 124 |
125 |
126 |
127 |
128 |
129 | ); 130 | } 131 | -------------------------------------------------------------------------------- /src/app/[locale]/copilot/page.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from "next-intl"; 2 | 3 | import type { Metadata } from "next"; 4 | import { createTranslator } from "next-intl"; 5 | 6 | type Props = { 7 | params: { locale: string }; 8 | }; 9 | export async function generateMetadata({ 10 | params: { locale }, 11 | }: Props): Promise { 12 | const messages = (await import(`../../../i18n/locales/${locale}.json`)).default; 13 | const t = createTranslator({ locale, messages }); 14 | 15 | return { 16 | title: t("Home.Copilot") + " - " + t("Home.Title"), 17 | description: t("Home.Copilot") + " - " + t("Home.Title"), 18 | }; 19 | } 20 | 21 | export default function Copilot() { 22 | const t = useTranslations("Copilot"); 23 | return ( 24 |
25 | {/* BG Image */} 26 | 31 | {/* Container */} 32 |
33 | {/* Component */} 34 |
35 | {/* Heading Content */} 36 |
37 |

38 | {t("FastSimpleAndSecure")} 39 |

40 |

41 | {t("CopilotDescription")} 42 |

43 | 47 | {t("DownloadClient")} 48 | 49 | 50 | 54 | {t("GetSourceCode")} 55 | 56 | 57 | {/* */} 76 |
77 | {/* Image Div */} 78 |
79 | 84 |
85 |
86 |
87 | 88 |
89 | {/* Background Image */} 90 | 95 | {/* Container */} 96 |
97 | {/* Heading Div */} 98 |
99 |

100 | {t("TutorialStep")} 101 |

102 |

103 | {t("HowUse")} 104 |

105 |
106 | {/* How it Works */} 107 |
108 | {/* Item */} 109 |
110 |
111 |

1

112 |
113 |

114 | {t("TutorialStep1Title")} 115 |

116 |

117 | {t("TutorialStep1Description")} 118 |

119 |
120 | {/* Item */} 121 |
122 |
123 |

2

124 |
125 |

126 | {t("TutorialStep2Title")} 127 |

128 |

129 | {t("TutorialStep2Description")} 130 |

131 |
132 | {/* Item */} 133 |
134 |
135 |

3

136 |
137 |

138 | {t("TutorialStep3Title")} 139 |

140 |

141 | {t("TutorialStep3Description")} 142 |

143 |
144 |
145 |
146 |
147 | 148 |
149 |

{t("HowUse")}

150 | 155 |
156 |
157 | ); 158 | } 159 | -------------------------------------------------------------------------------- /src/i18n/locales/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "Home": { 3 | "Title": "剪映一键关键帧", 4 | "Description": "一键设定剪映视频关键帧,轻松添加转场动画。", 5 | "Keywords": "剪映一键关键帧,剪映一秒批量打关键帧,剪映自动打关键帧,剪映关键帧", 6 | "TutorialStep": "只 需 三 步", 7 | "HowUse": "怎么使用", 8 | "TutorialStep1Title": "点击“选择剪映草稿文件”按钮", 9 | "TutorialStep1Description": "点击上方的“选择剪映草稿文件”按钮,选择你的剪映草稿文件。(注意:需要关闭剪映再操作)", 10 | "TutorialStep2Title": "选择你的剪映草稿文件", 11 | "TutorialStep2Description": "找到草稿文件夹,选择 `draft_info.json` 或 `draft_content.json` 文件。", 12 | "TutorialStep3Title": "等待自动下载到电脑中", 13 | "TutorialStep3Description": "等待处理完成后,会自动下载到你的电脑中。替换掉原来对应的并且重新打开剪映。", 14 | "SelectDraft": "选择剪映草稿文件", 15 | "JianyingProOneClickKeyframe": "剪映一键关键帧", 16 | "JianyingProBatchKeyframeinOneSecond": "剪映一秒批量打关键帧", 17 | "JianyingProAutomaticKeyframing": "剪映自动打关键帧", 18 | "Donate": "捐赠", 19 | "Home": "首页", 20 | "Copilot": "客户端", 21 | "Download": "下载", 22 | "Changelog": "更新日志", 23 | "NovelHotList": "小说热榜" 24 | }, 25 | "Faq": { 26 | "FaqTitle": "常见问题", 27 | "FaqDescription": " ", 28 | "List": [ 29 | { 30 | "Question": "剪映草稿文件无法读取怎么办?", 31 | "Answer": "最新通知:请下载剪映 5.8.0 版本才能使用剪映一键关键帧功能(由于剪映更新后对草稿文件进行了加密所以只能使用 5.8.0 版本的客户端) 下载地址: https://caiyun.139.com/m/i?2f2Tf9XM1p6b4 提取码:7qbg" 32 | }, 33 | { 34 | "Question": "我已经使用了 5.8.0 版本的剪映,但是还是无法读取草稿文件?", 35 | "Answer": "这是因为你现有的草稿文件已经被加密。即使降级到 5.8.0 版本,已加密的草稿文件也无法读取。解决方法是:使用 5.8.0 版本重新创建一个新的草稿文件进行编辑。" 36 | }, 37 | { 38 | "Question": "剪映一键关键帧是什么?", 39 | "Answer": "剪映一键关键帧功能是一种视频编辑工具,用于创建动画效果或者变换效果。当您需要在视频片段中提高视觉效果或者强调某一点时,关键帧可以帮助您实现。剪映的一键关键帧功能让这一复杂的过程变得更简单,用户只需要选择自己希望动画开始和结束的位置,即可自动创建关键帧。" 40 | }, 41 | { 42 | "Question": "如何使用剪映一键关键帧?", 43 | "Answer": "在剪映应用中打开您要编辑的视频片段,找到底部的关键帧图标并点击。然后在视频的特定点设置开始和结束的关键帧。完成后,您可以调节关键帧之间的动画效果,如平移、缩放等。最后预览并保存更改。这就是使用剪映一键关键帧的方法。" 44 | }, 45 | { 46 | "Question": "剪映一键关键帧可以创建哪些效果?", 47 | "Answer": "使用剪映一键关键帧,您可以创建各种动画效果,如平移、旋转、缩放等。此外,还可以根据需要改变透明度,或者在同一视频片段中创建多个动画效果。这些功能使您有更大的空间来增强视频的视觉效果,提高观众的观看体验。" 48 | }, 49 | { 50 | "Question": "在剪映中删除关键帧需要哪些步骤?", 51 | "Answer": "如果您在剪映中创建了关键帧,但稍后发现不需要它,您可以执行以下步骤删除关键帧:打开视频编辑页面,找到并点击底部的关键帧图标。然后,找到不需要的关键帧,并点击选中,最后点击删除即可。" 52 | }, 53 | { 54 | "Question": "剪映一键关键帧功能有何限制?", 55 | "Answer": "虽然剪映一键关键帧功能非常强大,但它也有一些限制。例如,一键关键帧并不能用于所有类型的视频编辑。它更适合用于创建动画效果,而不是大规模的视频编辑。此外,复杂的动画效果可能需要更多的时间和精力来创建,尤其是对于初学者来说。" 56 | }, 57 | { 58 | "Question": "是否所有的剪映版本都有一键关键帧功能?", 59 | "Answer": "剪映作为一款高效且实用的视频编辑应用,不断推出新功能给用户更好的使用体验。剪映一键关键帧是新添加的功能,可能不是所有版本都拥有。建议用户保持应用程序的更新,以便尽快获得最新的功能和优化。" 60 | } 61 | ], 62 | "Contact": "找不到你要找的答案?请联系我们的" 63 | }, 64 | "Donate": { 65 | "BuyTheAuthorACupOfCoffee": "请作者喝杯咖啡", 66 | "OneCoffee": "一杯咖啡", 67 | "OneMilkyTea": "一杯奶茶", 68 | "Custom": "定制", 69 | "DonateOneCoffeeDescription": "如果你喜欢他,就请他喝杯咖啡吧", 70 | "DonateOneMilkyTeaDescription": "如果你喜欢他,就请他喝杯奶茶吧", 71 | "DonateCustomDescription": "如果你喜欢他,就请他喝杯咖啡或奶茶吧", 72 | "DonateUnit": "杯", 73 | "DonatePlatformAfdian": "使用 爱发电", 74 | "DonatePlatformBuyMeACoffee": "使用 BuyMeACoffee" 75 | }, 76 | "Copilot": { 77 | "FastSimpleAndSecure": "快速,简单,安全", 78 | "CopilotDescription": "让一切变得更简单:一键完成关键帧!再也不用繁琐的手动操作——只需一个简单的网页界面和一个轻量级客户端来控制您的文件。而且,每次操作都会备份,确保您的数据安全。", 79 | "DownloadClient": "下载客户端", 80 | "GetSourceCode": "获取源码", 81 | "TutorialStep": "只 需 三 步", 82 | "HowUse": "怎么使用", 83 | "TutorialStep1Title": "下载并运行客户端", 84 | "TutorialStep1Description": "下载客户端并打开启动服务。", 85 | "TutorialStep2Title": "打开客户端操作台", 86 | "TutorialStep2Description": "按照客户端的提示打开操作台,或在浏览器中打开客户端的操作台: https://keyframeai.top/copilot/dashboard", 87 | "TutorialStep3Title": "选择草稿并处理", 88 | "TutorialStep3Description": "在控制台中选择草稿并点击 “一键处理草稿” 按钮,客户端将自动处理草稿并生成动画效果。" 89 | }, 90 | "CopilotDashboard": { 91 | "CheckingCopilotTips": "正在检测客户端是否启动...", 92 | "CheckingCopilotHelpTips": "加载不出来?点这里", 93 | "DownloadClient": "下载客户端", 94 | "SelectDraft": "选择一个草稿", 95 | "UnselectedDraftTitle": "请选择一个草稿文件", 96 | "AddSuccessTitle": "添加成功", 97 | "AddSuccessText": "请在剪映中打开草稿查看效果", 98 | "AddButton": "一键处理草稿", 99 | "WarningNotice": "注意:", 100 | "WarningNoticeText": "当前客户端版本属于公测阶段,可能存在一些问题,如遇问题请联系我们。 可以通过左下角的在线客服或者 E-mail: feedback@keyframeai.top 联系我们" 101 | }, 102 | "CopilotDwonload": { 103 | "Download": "下载" 104 | }, 105 | "DraftSetting": { 106 | "Auto": "自动", 107 | "DraftSetting": "草稿处理设置", 108 | "VideoRatio": "视频比例(默认情况遵照剪映中设置的比例)", 109 | "keyframeSpeed": "关键帧速度(建议范围 1-10)", 110 | "inKeyframeType": "选择关键帧类型(不选则不会添加关键帧)", 111 | "isRandomInAnimation": "开启随机入场动画", 112 | "isClearKeyframes": "开启清理旧关键帧(会删除原有的关���后重新生成)", 113 | "inAnimation": "选择入场动画", 114 | "isInAnimation": "是否添加入场动画", 115 | "isClearAnimations": "开启清理动画(会删除原有的动画数据)", 116 | "PleaseSelectInAnimation": "请选择入场动画", 117 | "inAnimationSpeed": "入场动画速度(单位:毫秒 1s = 1000ms)", 118 | "LargeToSmall": "大=>小", 119 | "SmallToLarge": "小=>大", 120 | "LeftToRight": "左=>右", 121 | "RightToLeft": "右=>左", 122 | "TopToBottom": "上=>下", 123 | "BottomToTop": "下=>上" 124 | }, 125 | "Footer": { 126 | "about": "关于", 127 | "followUs": "关注我们", 128 | "privacyPolicy": "隐私协议", 129 | "termsConditions": "使用条款", 130 | "legal": "条款", 131 | "Roadmap": "功能开发计划", 132 | "Donate": "捐赠" 133 | }, 134 | "NovelList": { 135 | "Link": "链接", 136 | "NovelListTitle": "热门小说列表", 137 | "NovelListDescription": "该功能目前正在测试中,需要定制数据或爆款推送可发送邮件到 contact@keyframeai.top 申请", 138 | "Nickname": "用户昵称", 139 | "Description": "描述", 140 | "Like": "喜欢", 141 | "Comment": "评论", 142 | "Collect": "收藏", 143 | "Share": "分享", 144 | "Action": "操作", 145 | "Show More": "查看更多", 146 | "Detail": "详情", 147 | "CreateTime": "发布时间", 148 | "Hot Index": "热度" 149 | }, 150 | "Leaderboard": { 151 | "pageTitle": "小说排行榜", 152 | "pageDescription": "发现不同类别最受欢迎的小说", 153 | "hotRanking": "热门榜", 154 | "likesRanking": "点赞榜", 155 | "commentsRanking": "评论榜", 156 | "favoritesRanking": "收藏榜", 157 | "sharesRanking": "分享榜", 158 | "latestRanking": "最新榜", 159 | "nickname": "用户昵称", 160 | "description": "描述", 161 | "hotIndex": "热度", 162 | "action": "操作", 163 | "link": "链接", 164 | "detail": "详情", 165 | "timeRange": "时间范围", 166 | "last7Days": "最近7天", 167 | "last14Days": "最近14天", 168 | "last30Days": "最近30天", 169 | "likes": "点赞数", 170 | "comments": "评论数", 171 | "collects": "收藏数", 172 | "shares": "分享数", 173 | "publishTime": "发布时间", 174 | "lastUpdateTime": "数据最后更新时间", 175 | "viewFullRanking": "查看完整榜单", 176 | "fullRankingComingSoon": "完整榜单功能开发中", 177 | "invalidRankingType": "无效的排行榜类型", 178 | "backToLeaderboard": "返回排行榜", 179 | "ranking": "排名", 180 | "details": "详情", 181 | "topicDetails": { 182 | "backToPrevious": "返回", 183 | "published": "发布时间", 184 | "likes": "点赞", 185 | "comments": "评论", 186 | "collects": "收藏", 187 | "shares": "分享", 188 | "hotIndex": "热度指数", 189 | "topicId": "话题 ID", 190 | "link": "链接", 191 | "lastUpdated": "最后更新", 192 | "author": "作者" 193 | } 194 | }, 195 | "Pagination": { 196 | "previous": "上一页", 197 | "next": "下一页" 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/app/[locale]/leaderboard/fullranking/[type]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { getTranslations } from 'next-intl/server'; 3 | import Link from 'next/link'; 4 | import BackToTop from '../../../components/BackToTop'; 5 | import TimeFilter from '../../components/TimeFilter'; 6 | import Pagination from './components/Pagination'; 7 | 8 | // 类型定义 9 | interface NovelItem { 10 | id: string; 11 | date_created: string; 12 | hot_index: number; 13 | nickname: string; 14 | description?: string; 15 | topic_link: string; 16 | topic_like?: number; 17 | topic_comment?: number; 18 | topic_collect?: number; 19 | topic_share?: number; 20 | topic_created_time?: string; 21 | } 22 | 23 | interface PageProps { 24 | params: { 25 | type: string; 26 | }; 27 | searchParams: { 28 | page?: string; 29 | days?: string; 30 | }; 31 | } 32 | 33 | // 生成元数据 34 | export async function generateMetadata({ params }: PageProps): Promise { 35 | const t = await getTranslations('Leaderboard'); 36 | 37 | const rankingTypes: { [key: string]: string } = { 38 | hot: t('hotRanking'), 39 | likes: t('likesRanking'), 40 | comments: t('commentsRanking'), 41 | favorites: t('favoritesRanking'), 42 | shares: t('sharesRanking'), 43 | latest: t('latestRanking'), 44 | }; 45 | 46 | return { 47 | title: `${rankingTypes[params.type] || t('fullRanking')} - ${t('pageTitle')}`, 48 | description: t('pageDescription'), 49 | }; 50 | } 51 | 52 | // 获取数据的函数 53 | async function getNovelData(type: string, page: number = 1, days: number = 7) { 54 | const apiUrl = "https://directus.keyframeai.top"; 55 | const limit = 20; // 每页显示数量 56 | const offset = (page - 1) * limit; 57 | 58 | // 计算日期范围 59 | const endDate = new Date().toISOString(); 60 | const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); 61 | 62 | try { 63 | const response = await fetch( 64 | `${apiUrl}/items/datas?limit=${limit}&offset=${offset}&sort[]=-${type}&sort[]=-topic_created_time&filter[date_created][_between]=${startDate},${endDate}`, 65 | { next: { revalidate: 3600 } } 66 | ); 67 | 68 | if (!response.ok) { 69 | throw new Error(`HTTP error! status: ${response.status}`); 70 | } 71 | 72 | const data = await response.json(); 73 | return { 74 | items: data.data || [], 75 | total: data.meta?.total_count || 0, 76 | }; 77 | } catch (error) { 78 | console.error('Error fetching novel data:', error); 79 | return { items: [], total: 0 }; 80 | } 81 | } 82 | 83 | export default async function FullRankingPage({ params, searchParams }: PageProps) { 84 | const t = await getTranslations('Leaderboard'); 85 | 86 | const currentPage = Number(searchParams?.page) || 1; 87 | const days = Number(searchParams?.days) || 7; 88 | const validDays = [7, 14, 30].includes(days) ? days : 7; 89 | 90 | const { type } = params; 91 | const validTypes = ['hot', 'likes', 'comments', 'favorites', 'shares', 'latest']; 92 | if (!validTypes.includes(type)) { 93 | return
{t('invalidRankingType')}
; 94 | } 95 | 96 | const fieldMapping: { [K in typeof type]: keyof NovelItem } = { 97 | hot: 'hot_index', 98 | likes: 'topic_like', 99 | comments: 'topic_comment', 100 | favorites: 'topic_collect', 101 | shares: 'topic_share', 102 | latest: 'topic_created_time', 103 | } as const; 104 | 105 | const titleMapping = { 106 | hot: t('hotRanking'), 107 | likes: t('likesRanking'), 108 | comments: t('commentsRanking'), 109 | favorites: t('favoritesRanking'), 110 | shares: t('sharesRanking'), 111 | latest: t('latestRanking'), 112 | }; 113 | 114 | const { items, total } = await getNovelData(fieldMapping[type as keyof typeof fieldMapping], currentPage, validDays); 115 | const hasNextPage = items.length === 20; // 如果当前页面有20条数据,说明可能还有下一页 116 | 117 | return ( 118 |
119 |
120 |
121 | 125 | ← {t('backToLeaderboard')} 126 | 127 |

128 | {titleMapping[type as keyof typeof titleMapping]} 129 |

130 |
131 | 132 | 133 | 134 |
135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | {type !== 'hot' && ( 143 | 146 | )} 147 | 148 | 149 | 150 | 151 | {items.map((item: NovelItem, index: number) => ( 152 | 153 | 156 | 157 | 161 | 172 | {type !== 'hot' && ( 173 | 178 | )} 179 | 197 | 198 | ))} 199 | 200 |
{t('ranking')}{t('nickname')}{t('description')}{t('hotIndex')} 144 | {titleMapping[type as keyof typeof titleMapping]} 145 | {t('action')}
154 | #{(currentPage - 1) * 20 + index + 1} 155 | {item.nickname} 158 | {item.description?.slice(0, 45)} 159 | {(item.description?.length ?? 0) > 45 ? '...' : ''} 160 | 5000 163 | ? 'text-red-500' 164 | : item.hot_index > 3000 165 | ? 'text-orange-500' 166 | : item.hot_index > 1000 167 | ? 'text-yellow-500' 168 | : 'text-[#636262]' 169 | } font-bold`}> 170 | {item.hot_index} 171 | 174 | {type === 'latest' 175 | ? new Date(item[fieldMapping[type as keyof typeof fieldMapping]] || Date.now()).toLocaleString() 176 | : item[fieldMapping[type as keyof typeof fieldMapping]]?.toLocaleString()} 177 | 180 |
181 | 187 | {t('link')} 188 | 189 | 193 | {t('details')} 194 | 195 |
196 |
201 |
202 | 203 | {items.length > 0 && ( 204 | 210 | )} 211 |
212 | 213 |
214 | ); 215 | } 216 | -------------------------------------------------------------------------------- /src/content/terms_en_US.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Terms of Service 3 | --- 4 | 5 | # Terms of Service 6 | 7 | ---- 8 | 9 | Last Updated: October 4, 2024 10 | 11 | Please read these terms and conditions carefully before using our services. 12 | 13 | ## Interpretation and Definitions 14 | 15 | ### Interpretation 16 | 17 | Words with capital letters have specific meanings in the following conditions. The following definitions have the same meaning in singular or plural form. 18 | 19 | ### Definitions 20 | 21 | For the purposes of these terms and conditions: 22 | 23 | - __Affiliate__ refers to an entity that controls, is controlled by, or is under common control with a party, where “control” means ownership of 50% or more of the shares, equity, or other securities that are entitled to vote for the election of directors or other managing authority. 24 | 25 | - __Country__ refers to: the State of Oregon, United States. 26 | - __Company__ (referred to in this agreement as “Company”, “We”, “Us”, or “Our”) refers to Keyframeai. 27 | 28 | - __Device__ refers to any device that can access the service, such as a computer, mobile phone, or tablet. 29 | 30 | - __Service__ refers to the website. 31 | 32 | - __Terms and Conditions__ (also referred to as “Terms”) refers to these terms and conditions, which form the complete agreement between you and the Company for using the service. 33 | - __Third-Party Social Media Services__ refers to any services or content (including data, information, products, or services) provided by third parties that may be displayed, included, or offered through the service. 34 | - __Website__ refers to Keyframeai, accessible at [https://keyframeai.top/](https://keyframeai.top/). 35 | - __You__ refers to the individual accessing or using the service, or the company or other legal entity accessing or using the service on behalf of that individual (if applicable). 36 | 37 | ## Confirmation 38 | 39 | These terms and conditions govern your agreement with the Company for using the service. These terms and conditions set forth the rights and obligations of all users in using the service. 40 | 41 | Your access to and use of the service is contingent upon your acceptance and compliance with these terms and conditions. These terms and conditions apply to all visitors, users, and others who access or use the service. 42 | 43 | By accessing or using the service, you agree to be bound by these terms and conditions. If you do not agree with any part of these terms and conditions, you may not access the service. 44 | 45 | You represent that you are at least 18 years old. The Company does not permit anyone under 18 to use the service. 46 | 47 | Your access to and use of the service is also contingent upon your acceptance and compliance with the Company's privacy policy. Our privacy policy describes our policies and procedures for collecting, using, and disclosing your personal information when you use our application or website and informs you about your privacy rights and how the law protects you. Please read our privacy policy carefully before using our services. 48 | 49 | ## Links to Other Websites 50 | 51 | Our service may contain links to third-party websites or services that are not owned or controlled by the Company. 52 | 53 | The Company has no control over the content, privacy policies, or practices of any third-party websites or services and assumes no responsibility. You further acknowledge and agree that the Company shall not be liable for any damage or loss caused or claimed to be caused by or in connection with the use of or reliance on any such content, goods, or services available on or through any such websites or services. 54 | 55 | We strongly advise you to read the terms and conditions and privacy policy of any third-party website or service that you visit. 56 | 57 | ## Termination 58 | 59 | We may terminate or suspend your access immediately, without prior notice or liability, for any reason, including but not limited to your breach of these terms and conditions. 60 | 61 | Upon termination, your right to use the service will immediately cease. 62 | 63 | ## Limitation of Liability 64 | 65 | To the fullest extent permitted by law, in no event shall the Company or its suppliers be liable for any special, incidental, indirect, or consequential damages (including but not limited to loss of profits, data, or other information) arising out of or in connection with your use of or inability to use the service, whether based on warranty, contract, tort (including negligence), or any other legal theory, even if the Company has been advised of the possibility of such damages. 66 | 67 | Some jurisdictions do not allow the exclusion of implied warranties or limitation of incidental or consequential damages, which means that some of the above limitations may not apply to you. In these jurisdictions, each party's liability will be limited to the maximum extent permitted by law. 68 | 69 | ## “As Is” and “As Available” Disclaimer 70 | 71 | The service is provided on an “as is” and “as available” basis, with all defects and imperfections. To the fullest extent permitted by applicable law, the Company, on its own behalf and on behalf of its affiliates and its respective licensors and service providers, expressly disclaims all warranties of any kind, whether express, implied, statutory, or otherwise, including all implied warranties of merchantability, fitness for a particular purpose, title, and non-infringement, and any warranties that may arise from the course of dealing, performance, usage, or trade practice. Without limiting the foregoing, the Company does not provide any warranty or commitment, and makes no representation that the service will meet your requirements, achieve any intended results, be compatible or work with any other software, applications, systems, or services, operate without interruption, meet any performance or reliability standards, or be error-free, or that any errors or defects can or will be corrected. 72 | 73 | Without limiting the foregoing, neither the Company nor any provider of the Company makes any warranty or representation of any kind: (i) regarding the operation or availability of the service, or the information, content, and materials or products contained therein; (ii) that the service will be uninterrupted or error-free; (iii) regarding the accuracy, reliability, or timeliness of any information or content provided through the service; or (iv) that the service, its servers, content, or emails sent on behalf of the Company are free of viruses, scripts, trojan horses, worms, malware, time bombs, or other harmful components. 74 | 75 | Some jurisdictions do not allow the exclusion of certain types of warranties or limitations on statutory rights applicable to consumers, so some or all of the above exclusions and limitations may not apply to you. But in such cases, the exclusions and limitations set forth in this section shall apply to the maximum extent permitted by law. 76 | 77 | ## Governing Law 78 | 79 | These terms and your use of the service shall be governed by the laws of the Country, without regard to its conflict of law principles. Your use of the application may also be subject to other local, state, national, or international laws. 80 | 81 | ## Dispute Resolution 82 | 83 | If you have any concerns or disputes regarding the service, you agree to first try to resolve the dispute informally by contacting the Company. 84 | 85 | ## Applicable to Users in the European Union (EU) 86 | 87 | If you are a consumer in the European Union, you will benefit from any mandatory provisions of the law of the country in which you reside. 88 | 89 | ## Compliance with U.S. Law 90 | 91 | You represent and warrant that: (i) you are not located in a country that is subject to a U.S. government embargo, or that has been designated by the U.S. government as a “supporting terrorism” country; (ii) you are not listed on any U.S. government list of prohibited or restricted parties. 92 | 93 | ## Severability and Waiver 94 | 95 | ### Severability 96 | 97 | If any provision of these terms is held to be unenforceable or invalid, that provision will be modified and interpreted to achieve its intent to the maximum extent permitted by applicable law, and the remaining provisions will continue to be in full force and effect. 98 | 99 | ### Waiver 100 | 101 | Except as otherwise provided in these terms, the failure to exercise any right or demand the performance of any obligation will not affect a party's ability to exercise that right or demand the performance of that obligation at any later time, nor shall it constitute a waiver of any subsequent breach. 102 | 103 | ## Translation Interpretation 104 | 105 | If we have provided you with these terms and conditions, they may have been translated. You agree that in the event of a dispute, the original English text shall prevail. 106 | 107 | ## Modifications to These Terms and Conditions 108 | 109 | We reserve the right to modify or replace these terms at any time at our sole discretion. If the changes are substantial, we will make reasonable efforts to notify you at least 30 days before the new terms take effect. What constitutes a substantial change will be determined by us at our sole discretion. 110 | 111 | By continuing to access or use our service after those revisions become effective, you agree to be bound by the revised terms. If you do not agree to the new terms (in whole or in part), please stop using the website and the service. 112 | 113 | ## Contact Us 114 | 115 | If you have any questions about these terms and conditions, please contact us at: 116 | 117 | - Email: contact@keyframeai.top 118 | -------------------------------------------------------------------------------- /src/app/[locale]/leaderboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { useTranslations } from 'next-intl'; 3 | import { getTranslations } from 'next-intl/server'; 4 | import BackToTop from '../components/BackToTop'; 5 | import TimeFilter from './components/TimeFilter'; 6 | import Link from 'next/link'; 7 | 8 | export async function generateMetadata(): Promise { 9 | const t = await getTranslations('Leaderboard'); 10 | 11 | return { 12 | title: t('pageTitle'), 13 | description: t('pageDescription'), 14 | openGraph: { 15 | title: t('pageTitle'), 16 | description: t('pageDescription'), 17 | }, 18 | }; 19 | } 20 | 21 | async function getNovelData(type: string, days: number = 7) { 22 | const apiUrl = "https://directus.keyframeai.top"; 23 | 24 | // 计算日期范围 25 | const endDate = new Date().toISOString(); 26 | const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); 27 | 28 | try { 29 | const response = await fetch( 30 | `${apiUrl}/items/datas?limit=10&sort[]=-${type}&sort[]=-topic_created_time&filter[date_created][_between]=${startDate},${endDate}`, 31 | { next: { revalidate: 3600 } } 32 | ); 33 | 34 | if (!response.ok) { 35 | throw new Error(`HTTP error! status: ${response.status}`); 36 | } 37 | 38 | const data = await response.json(); 39 | return data.data || []; 40 | } catch (error) { 41 | console.error('Error fetching novel data:', error); 42 | return []; // 返回空数组作为默认值 43 | } 44 | } 45 | 46 | // 首先添加类型定义 47 | interface NovelItem { 48 | id: string; 49 | date_created: string; 50 | hot_index: number; 51 | nickname: string; 52 | description?: string; 53 | topic_link: string; 54 | topic_like?: number; 55 | topic_comment?: number; 56 | topic_collect?: number; 57 | topic_share?: number; 58 | topic_created_time?: string; 59 | } 60 | 61 | interface Ranking { 62 | id: string; 63 | title: string; 64 | data: NovelItem[]; 65 | showField: string; 66 | fieldTitle: string; 67 | hideField?: boolean; 68 | } 69 | 70 | export default async function LeaderboardPage({ 71 | searchParams = { days: '7' }, 72 | }: { 73 | searchParams?: { days?: string }; 74 | }) { 75 | const t = await getTranslations('Leaderboard'); 76 | 77 | const days = Number(searchParams?.days) || 7; 78 | const validDays = [7, 14, 30].includes(days) ? days : 7; 79 | 80 | const rankings: Ranking[] = [ 81 | { 82 | id: "hot", 83 | title: t('hotRanking'), 84 | data: await getNovelData('hot_index', validDays), 85 | showField: 'hot_index', 86 | fieldTitle: t('hotIndex'), 87 | hideField: true 88 | }, 89 | { 90 | id: "likes", 91 | title: t('likesRanking'), 92 | data: await getNovelData('topic_like', validDays), 93 | showField: 'topic_like', 94 | fieldTitle: t('likes') 95 | }, 96 | { 97 | id: "comments", 98 | title: t('commentsRanking'), 99 | data: await getNovelData('topic_comment', validDays), 100 | showField: 'topic_comment', 101 | fieldTitle: t('comments') 102 | }, 103 | { 104 | id: "favorites", 105 | title: t('favoritesRanking'), 106 | data: await getNovelData('topic_collect', validDays), 107 | showField: 'topic_collect', 108 | fieldTitle: t('collects') 109 | }, 110 | { 111 | id: "shares", 112 | title: t('sharesRanking'), 113 | data: await getNovelData('topic_share', validDays), 114 | showField: 'topic_share', 115 | fieldTitle: t('shares') 116 | }, 117 | { 118 | id: "latest", 119 | title: t('latestRanking'), 120 | data: await getNovelData('topic_created_time', validDays), 121 | showField: 'topic_created_time', 122 | fieldTitle: t('publishTime') 123 | } 124 | ]; 125 | 126 | const lastUpdateTime = rankings.reduce((latestTime: Date, ranking: Ranking) => { 127 | const rankingLatestTime = ranking.data.reduce((maxTime: Date, item: NovelItem) => { 128 | const itemTime = new Date(item.date_created); 129 | return maxTime > itemTime ? maxTime : itemTime; 130 | }, new Date(0)); 131 | 132 | return latestTime > rankingLatestTime ? latestTime : rankingLatestTime; 133 | }, new Date(0)); 134 | 135 | return ( 136 |
137 |
138 |

{t('pageTitle')}

139 | 140 |
141 | {t('lastUpdateTime')}: {lastUpdateTime.toLocaleString()} 142 |
143 | 144 | {/* Time Filter */} 145 | 146 | 147 | {/* Navigation */} 148 | 159 | 160 | {/* Rankings */} 161 |
162 | {rankings.map((ranking) => ( 163 |
168 |

169 | {ranking.title} 170 |

171 |
172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | {!ranking.hideField && ( 180 | 181 | )} 182 | 183 | 184 | 185 | 186 | {ranking.data.map((item: any, index: number) => ( 187 | 188 | 189 | 190 | 194 | 205 | {!ranking.hideField && ( 206 | 211 | )} 212 | 228 | 229 | ))} 230 | 231 |
排名{t('nickname')}{t('description')}{t('hotIndex')}{ranking.fieldTitle}{t('action')}
#{index + 1}{item.nickname} 191 | {item.description?.slice(0, 45)} 192 | {item.description?.length > 45 ? '...' : ''} 193 | 5000 196 | ? 'text-red-500' 197 | : item.hot_index > 3000 198 | ? 'text-orange-500' 199 | : item.hot_index > 1000 200 | ? 'text-yellow-500' 201 | : 'text-[#636262]' 202 | } font-bold`}> 203 | {item.hot_index} 204 | 207 | {ranking.id === 'latest' 208 | ? new Date(item[ranking.showField]).toLocaleString() 209 | : item[ranking.showField]?.toLocaleString()} 210 | 213 | 219 | {t('link')} 220 | 221 | 225 | {t('detail')} 226 | 227 |
232 |
233 |
234 | 238 | {t('viewFullRanking')} 239 | 240 |
241 |
242 | ))} 243 |
244 |
245 | 246 |
247 | ); 248 | } 249 | -------------------------------------------------------------------------------- /src/jianying/effects/animations.ts: -------------------------------------------------------------------------------- 1 | export const InAnimations = { 2 | "431636": { 3 | "anim_adjust_params": null, 4 | "category_id": "in", 5 | "category_name": "入场", 6 | "duration": 500000, 7 | "id": "431636", 8 | "material_type": "video", 9 | "name": "向右甩入", 10 | "panel": "video", 11 | "platform": "all", 12 | "request_id": "2024091516421720A619E8C1381BFF2B23", 13 | "resource_id": "6739338727866241539", 14 | "start": 0, 15 | "type": "in" 16 | }, 17 | "431638": { 18 | "anim_adjust_params": null, 19 | "category_id": "in", 20 | "category_name": "入场", 21 | "duration": 500000, 22 | "id": "431638", 23 | "material_type": "video", 24 | "name": "向下甩入", 25 | "panel": "video", 26 | "platform": "all", 27 | "request_id": "2024091516421720A619E8C1381BFF2B23", 28 | "resource_id": "6739338374441603598", 29 | "start": 0, 30 | "type": "in" 31 | }, 32 | "431640": { 33 | "anim_adjust_params": null, 34 | "category_id": "in", 35 | "category_name": "入场", 36 | "duration": 500000, 37 | "id": "431640", 38 | "material_type": "video", 39 | "name": "向右下甩入", 40 | "panel": "video", 41 | "platform": "all", 42 | "request_id": "2024091516421720A619E8C1381BFF2B23", 43 | "resource_id": "6739395718223499787", 44 | "start": 0, 45 | "type": "in" 46 | }, 47 | "431642": { 48 | "anim_adjust_params": null, 49 | "category_id": "in", 50 | "category_name": "入场", 51 | "duration": 500000, 52 | "id": "431642", 53 | "material_type": "video", 54 | "name": "向左下甩入", 55 | "panel": "video", 56 | "platform": "all", 57 | "request_id": "2024091516421720A619E8C1381BFF2B23", 58 | "resource_id": "6739395445346275853", 59 | "start": 0, 60 | "type": "in" 61 | }, 62 | "431644": { 63 | "anim_adjust_params": null, 64 | "category_id": "in", 65 | "category_name": "入场", 66 | "duration": 500000, 67 | "id": "431644", 68 | "material_type": "video", 69 | "name": "向右上甩入", 70 | "panel": "video", 71 | "platform": "all", 72 | "request_id": "2024091516421720A619E8C1381BFF2B23", 73 | "resource_id": "6740122731418751495", 74 | "start": 0, 75 | "type": "in" 76 | }, 77 | "431648": { 78 | "anim_adjust_params": null, 79 | "category_id": "in", 80 | "category_name": "入场", 81 | "duration": 500000, 82 | "id": "431648", 83 | "material_type": "video", 84 | "name": "向左上甩入", 85 | "panel": "video", 86 | "platform": "all", 87 | "request_id": "2024091516421720A619E8C1381BFF2B23", 88 | "resource_id": "6740122563692728844", 89 | "start": 0, 90 | "type": "in" 91 | }, 92 | "431650": { 93 | "anim_adjust_params": null, 94 | "category_id": "in", 95 | "category_name": "入场", 96 | "duration": 500000, 97 | "id": "431650", 98 | "material_type": "video", 99 | "name": "轻微抖动 II", 100 | "panel": "video", 101 | "platform": "all", 102 | "request_id": "2024091516421720A619E8C1381BFF2B23", 103 | "resource_id": "6739418677910704651", 104 | "start": 0, 105 | "type": "in" 106 | }, 107 | "431652": { 108 | "anim_adjust_params": null, 109 | "category_id": "in", 110 | "category_name": "入场", 111 | "duration": 500000, 112 | "id": "431652", 113 | "material_type": "video", 114 | "name": "上下抖动", 115 | "panel": "video", 116 | "platform": "all", 117 | "request_id": "2024091516421720A619E8C1381BFF2B23", 118 | "resource_id": "6739418390030455300", 119 | "start": 0, 120 | "type": "in" 121 | }, 122 | "431654": { 123 | "anim_adjust_params": null, 124 | "category_id": "in", 125 | "category_name": "入场", 126 | "duration": 500000, 127 | "id": "431654", 128 | "material_type": "video", 129 | "name": "左右抖动", 130 | "panel": "video", 131 | "platform": "all", 132 | "request_id": "2024091516421720A619E8C1381BFF2B23", 133 | "resource_id": "6739418540421419524", 134 | "start": 0, 135 | "type": "in" 136 | }, 137 | "431658": { 138 | "anim_adjust_params": null, 139 | "category_id": "in", 140 | "category_name": "入场", 141 | "duration": 500000, 142 | "id": "431658", 143 | "material_type": "video", 144 | "name": "动感缩小", 145 | "panel": "video", 146 | "platform": "all", 147 | "request_id": "2024091516421720A619E8C1381BFF2B23", 148 | "resource_id": "6740868384637850120", 149 | "start": 0, 150 | "type": "in" 151 | }, 152 | "431662": { 153 | "anim_adjust_params": null, 154 | "category_id": "in", 155 | "category_name": "入场", 156 | "duration": 500000, 157 | "id": "431662", 158 | "material_type": "video", 159 | "name": "动感放大", 160 | "panel": "video", 161 | "platform": "all", 162 | "request_id": "2024091516421720A619E8C1381BFF2B23", 163 | "resource_id": "6740867832570974733", 164 | "start": 0, 165 | "type": "in" 166 | }, 167 | "431664": { 168 | "anim_adjust_params": null, 169 | "category_id": "in", 170 | "category_name": "入场", 171 | "duration": 500000, 172 | "id": "431664", 173 | "material_type": "video", 174 | "name": "轻微抖动", 175 | "panel": "video", 176 | "platform": "all", 177 | "request_id": "2024091516421720A619E8C1381BFF2B23", 178 | "resource_id": "6739418227031413256", 179 | "start": 0, 180 | "type": "in" 181 | }, 182 | "503136": { 183 | "anim_adjust_params": null, 184 | "category_id": "in", 185 | "category_name": "入场", 186 | "duration": 500000, 187 | "id": "503136", 188 | "material_type": "video", 189 | "name": "轻微抖动 III", 190 | "panel": "video", 191 | "platform": "all", 192 | "request_id": "2024091516421720A619E8C1381BFF2B23", 193 | "resource_id": "6781683302672634382", 194 | "start": 0, 195 | "type": "in" 196 | }, 197 | "624705": { 198 | "anim_adjust_params": null, 199 | "category_id": "in", 200 | "category_name": "入场", 201 | "duration": 500000, 202 | "id": "624705", 203 | "material_type": "video", 204 | "name": "渐显", 205 | "panel": "video", 206 | "platform": "all", 207 | "request_id": "2024091516421720A619E8C1381BFF2B23", 208 | "resource_id": "6798320778182922760", 209 | "start": 0, 210 | "type": "in" 211 | }, 212 | "624755": { 213 | "anim_adjust_params": null, 214 | "category_id": "in", 215 | "category_name": "入场", 216 | "duration": 500000, 217 | "id": "624755", 218 | "material_type": "video", 219 | "name": "缩小", 220 | "panel": "video", 221 | "platform": "all", 222 | "request_id": "2024091516421720A619E8C1381BFF2B23", 223 | "resource_id": "6798332584276267527", 224 | "start": 0, 225 | "type": "in" 226 | }, 227 | "629085": { 228 | "anim_adjust_params": null, 229 | "category_id": "in", 230 | "category_name": "入场", 231 | "duration": 500000, 232 | "id": "629085", 233 | "material_type": "video", 234 | "name": "轻微放大", 235 | "panel": "video", 236 | "platform": "all", 237 | "request_id": "2024091516421720A619E8C1381BFF2B23", 238 | "resource_id": "6800268825611735559", 239 | "start": 0, 240 | "type": "in" 241 | }, 242 | "634681": { 243 | "anim_adjust_params": null, 244 | "category_id": "in", 245 | "category_name": "入场", 246 | "duration": 500000, 247 | "id": "634681", 248 | "material_type": "video", 249 | "name": "雨刷", 250 | "panel": "video", 251 | "platform": "all", 252 | "request_id": "2024091516421720A619E8C1381BFF2B23", 253 | "resource_id": "6802871256849846791", 254 | "start": 0, 255 | "type": "in" 256 | }, 257 | "636115": { 258 | "anim_adjust_params": null, 259 | "category_id": "in", 260 | "category_name": "入场", 261 | "duration": 500000, 262 | "id": "636115", 263 | "material_type": "video", 264 | "name": "钟摆", 265 | "panel": "video", 266 | "platform": "all", 267 | "request_id": "2024091516421720A619E8C1381BFF2B23", 268 | "resource_id": "6803260897117606414", 269 | "start": 0, 270 | "type": "in" 271 | }, 272 | "640101": { 273 | "anim_adjust_params": null, 274 | "category_id": "in", 275 | "category_name": "入场", 276 | "duration": 500000, 277 | "id": "640101", 278 | "material_type": "video", 279 | "name": "雨刷 II", 280 | "panel": "video", 281 | "platform": "all", 282 | "request_id": "2024091516421720A619E8C1381BFF2B23", 283 | "resource_id": "6805748897768542727", 284 | "start": 0, 285 | "type": "in" 286 | }, 287 | "645307": { 288 | "anim_adjust_params": null, 289 | "category_id": "in", 290 | "category_name": "入场", 291 | "duration": 500000, 292 | "id": "645307", 293 | "material_type": "video", 294 | "name": "向上转入", 295 | "panel": "video", 296 | "platform": "all", 297 | "request_id": "2024091516421720A619E8C1381BFF2B23", 298 | "resource_id": "6808401616564130312", 299 | "start": 0, 300 | "type": "in" 301 | }, 302 | "699157": { 303 | "anim_adjust_params": null, 304 | "category_id": "in", 305 | "category_name": "入场", 306 | "duration": 500000, 307 | "id": "699157", 308 | "material_type": "video", 309 | "name": "向左转入", 310 | "panel": "video", 311 | "platform": "all", 312 | "request_id": "2024091516421720A619E8C1381BFF2B23", 313 | "resource_id": "6816560956647150093", 314 | "start": 0, 315 | "type": "in" 316 | }, 317 | "701961": { 318 | "anim_adjust_params": null, 319 | "category_id": "in", 320 | "category_name": "入场", 321 | "duration": 500000, 322 | "id": "701961", 323 | "material_type": "video", 324 | "name": "向上转入 II", 325 | "panel": "video", 326 | "platform": "all", 327 | "request_id": "2024091516421720A619E8C1381BFF2B23", 328 | "resource_id": "6818747060649464327", 329 | "start": 0, 330 | "type": "in" 331 | }, 332 | "703281": { 333 | "anim_adjust_params": null, 334 | "category_id": "in", 335 | "category_name": "入场", 336 | "duration": 500000, 337 | "id": "703281", 338 | "material_type": "video", 339 | "name": "漩涡旋转", 340 | "panel": "video", 341 | "platform": "all", 342 | "request_id": "2024091516421720A619E8C1381BFF2B23", 343 | "resource_id": "6782010677520241165", 344 | "start": 0, 345 | "type": "in" 346 | } 347 | }; -------------------------------------------------------------------------------- /src/styles/markdown-dark.css: -------------------------------------------------------------------------------- 1 | .markdown-body h1, 2 | .markdown-body h2, 3 | .markdown-body h3, 4 | .markdown-body h4, 5 | .markdown-body h5, 6 | .markdown-body p, 7 | .markdown-body ul, 8 | .markdown-body ol, 9 | .markdown-body blockquote, 10 | .markdown-body pre, 11 | .markdown-body code, 12 | .markdown-body table, 13 | .markdown-body th, 14 | .markdown-body td { 15 | all: unset; 16 | display: revert; 17 | } 18 | 19 | /* Reset heading styles */ 20 | .markdown-body h1 { 21 | font-size: 2em; 22 | margin: 0.67em 0; 23 | } 24 | 25 | .markdown-body h2 { 26 | font-size: 1.5em; 27 | margin: 0.83em 0; 28 | } 29 | 30 | .markdown-body h3 { 31 | font-size: 1.17em; 32 | margin: 1em 0; 33 | } 34 | 35 | .markdown-body h4 { 36 | font-size: 1em; 37 | margin: 1.33em 0; 38 | } 39 | 40 | .markdown-body h5 { 41 | font-size: 0.83em; 42 | margin: 1.67em 0; 43 | } 44 | 45 | /* Reset paragraph styles */ 46 | .markdown-body p { 47 | margin: 1em 0; 48 | } 49 | 50 | /* Reset list styles */ 51 | .markdown-body ul { 52 | list-style-type: disc; 53 | margin-left: 1.5em; 54 | } 55 | 56 | .markdown-body ol { 57 | list-style-type: decimal; 58 | margin-left: 1.5em; 59 | } 60 | 61 | /* Reset blockquote styles */ 62 | .markdown-body blockquote { 63 | margin: 1em 0; 64 | padding-left: 1.5em; 65 | border-left: 0.25em solid #dfe2e5; 66 | } 67 | 68 | /* Reset code and pre styles */ 69 | .markdown-body pre { 70 | font-family: monospace, monospace; 71 | font-size: 1em; 72 | margin: 1em 0; 73 | white-space: pre; 74 | } 75 | 76 | .markdown-body code { 77 | font-family: monospace, monospace; 78 | font-size: 1em; 79 | background-color: #f5f5f5; 80 | padding: 0.2em 0.4em; 81 | border-radius: 3px; 82 | } 83 | 84 | /* Reset table styles */ 85 | .markdown-body table { 86 | border-collapse: collapse; 87 | width: 100%; 88 | } 89 | 90 | .markdown-body th, 91 | .markdown-body td { 92 | border: 1px solid #dfe2e5; 93 | padding: 0.5em; 94 | } 95 | 96 | .markdown-body th { 97 | background-color: #f9f9f9; 98 | text-align: left; 99 | } 100 | 101 | /* Reset anchor styles */ 102 | .markdown-body a { 103 | color: #0366d6; 104 | text-decoration: underline; 105 | } 106 | 107 | .markdown-body a:visited { 108 | color: #6f42c1; 109 | } 110 | 111 | .markdown-body a:hover { 112 | color: #0056b3; 113 | } 114 | 115 | /* Reset image styles */ 116 | .markdown-body img { 117 | max-width: 100%; 118 | height: auto; 119 | } 120 | 121 | /* markdown dark style */ 122 | .markdown-body { 123 | background: #000; 124 | font-family: Georgia, Palatino, serif; 125 | color: #EEE; 126 | line-height: 1; 127 | padding: 30px; 128 | margin: auto; 129 | max-width: 42em; 130 | } 131 | 132 | .markdown-body h1, 133 | .markdown-body h2, 134 | .markdown-body h3, 135 | .markdown-body h4 { 136 | font-weight: 400; 137 | } 138 | 139 | .markdown-body h1, 140 | .markdown-body h2, 141 | .markdown-body h3, 142 | .markdown-body h4, 143 | .markdown-body h5, 144 | .markdown-body p { 145 | margin-bottom: 24px; 146 | padding: 0; 147 | } 148 | 149 | .markdown-body h1 { 150 | font-size: 48px; 151 | } 152 | 153 | .markdown-body h2 { 154 | font-size: 36px; 155 | margin: 24px 0 6px; 156 | } 157 | 158 | .markdown-body h3 { 159 | font-size: 24px; 160 | } 161 | 162 | .markdown-body h4 { 163 | font-size: 21px; 164 | } 165 | 166 | .markdown-body h5 { 167 | font-size: 18px; 168 | } 169 | 170 | .markdown-body a { 171 | color: #61BFC1; 172 | margin: 0; 173 | padding: 0; 174 | text-decoration: none; 175 | vertical-align: baseline; 176 | } 177 | 178 | .markdown-body a:hover { 179 | text-decoration: underline; 180 | } 181 | 182 | .markdown-body a:visited { 183 | color: #466B6C; 184 | } 185 | 186 | .markdown-body ul, 187 | .markdown-body ol { 188 | padding: 0; 189 | margin: 0; 190 | } 191 | 192 | .markdown-body li { 193 | line-height: 24px; 194 | } 195 | 196 | .markdown-body li ul, 197 | .markdown-body li ol { 198 | margin-left: 24px; 199 | } 200 | 201 | .markdown-body p, 202 | .markdown-body ul, 203 | .markdown-body ol { 204 | font-size: 16px; 205 | line-height: 24px; 206 | max-width: 540px; 207 | } 208 | 209 | .markdown-body pre { 210 | padding: 0px 24px; 211 | max-width: 800px; 212 | white-space: pre-wrap; 213 | } 214 | 215 | .markdown-body code { 216 | font-family: Consolas, Monaco, Andale Mono, monospace; 217 | line-height: 1.5; 218 | font-size: 13px; 219 | } 220 | 221 | .markdown-body aside { 222 | display: block; 223 | float: right; 224 | width: 390px; 225 | } 226 | 227 | .markdown-body blockquote { 228 | border-left: .5em solid #eee; 229 | padding: 0 2em; 230 | margin-left: 0; 231 | max-width: 476px; 232 | } 233 | 234 | .markdown-body blockquote cite { 235 | font-size: 14px; 236 | line-height: 20px; 237 | color: #bfbfbf; 238 | } 239 | 240 | .markdown-body blockquote cite:before { 241 | content: '\2014 \00A0'; 242 | } 243 | 244 | .markdown-body blockquote p { 245 | color: #666; 246 | max-width: 460px; 247 | } 248 | 249 | .markdown-body hr { 250 | width: 540px; 251 | text-align: left; 252 | margin: 0 auto 0 0; 253 | color: #999; 254 | } 255 | 256 | /* Code below this line is copyright Twitter Inc. */ 257 | 258 | .markdown-body button, 259 | .markdown-body input, 260 | .markdown-body select, 261 | .markdown-body textarea { 262 | font-size: 100%; 263 | margin: 0; 264 | vertical-align: baseline; 265 | *vertical-align: middle; 266 | } 267 | 268 | .markdown-body button, 269 | .markdown-body input { 270 | line-height: normal; 271 | *overflow: visible; 272 | } 273 | 274 | .markdown-body button::-moz-focus-inner, 275 | .markdown-body input::-moz-focus-inner { 276 | border: 0; 277 | padding: 0; 278 | } 279 | 280 | .markdown-body button, 281 | .markdown-body input[type="button"], 282 | .markdown-body input[type="reset"], 283 | .markdown-body input[type="submit"] { 284 | cursor: pointer; 285 | -webkit-appearance: button; 286 | } 287 | 288 | .markdown-body input[type=checkbox], 289 | .markdown-body input[type=radio] { 290 | cursor: pointer; 291 | } 292 | 293 | /* override default chrome & firefox settings */ 294 | .markdown-body input:not([type="image"]), 295 | .markdown-body textarea { 296 | -webkit-box-sizing: content-box; 297 | -moz-box-sizing: content-box; 298 | box-sizing: content-box; 299 | } 300 | 301 | .markdown-body input[type="search"] { 302 | -webkit-appearance: textfield; 303 | -webkit-box-sizing: content-box; 304 | -moz-box-sizing: content-box; 305 | box-sizing: content-box; 306 | } 307 | 308 | .markdown-body input[type="search"]::-webkit-search-decoration { 309 | -webkit-appearance: none; 310 | } 311 | 312 | .markdown-body label, 313 | .markdown-body input, 314 | .markdown-body select, 315 | .markdown-body textarea { 316 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 317 | font-size: 13px; 318 | font-weight: normal; 319 | line-height: normal; 320 | margin-bottom: 18px; 321 | } 322 | 323 | .markdown-body input[type=checkbox], 324 | .markdown-body input[type=radio] { 325 | cursor: pointer; 326 | margin-bottom: 0; 327 | } 328 | 329 | .markdown-body input[type=text], 330 | .markdown-body input[type=password], 331 | .markdown-body textarea, 332 | .markdown-body select { 333 | display: inline-block; 334 | width: 210px; 335 | padding: 4px; 336 | font-size: 13px; 337 | font-weight: normal; 338 | line-height: 18px; 339 | height: 18px; 340 | color: #808080; 341 | border: 1px solid #ccc; 342 | -webkit-border-radius: 3px; 343 | -moz-border-radius: 3px; 344 | border-radius: 3px; 345 | } 346 | 347 | .markdown-body select, 348 | .markdown-body input[type=file] { 349 | height: 27px; 350 | line-height: 27px; 351 | } 352 | 353 | .markdown-body textarea { 354 | height: auto; 355 | } 356 | 357 | /* grey out placeholders */ 358 | .markdown-body :-moz-placeholder { 359 | color: #bfbfbf; 360 | } 361 | 362 | .markdown-body ::-webkit-input-placeholder { 363 | color: #bfbfbf; 364 | } 365 | 366 | .markdown-body input[type=text], 367 | .markdown-body input[type=password], 368 | .markdown-body select, 369 | .markdown-body textarea { 370 | -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; 371 | -moz-transition: border linear 0.2s, box-shadow linear 0.2s; 372 | transition: border linear 0.2s, box-shadow linear 0.2s; 373 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); 374 | -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); 375 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); 376 | } 377 | 378 | .markdown-body input[type=text]:focus, 379 | .markdown-body input[type=password]:focus, 380 | .markdown-body textarea:focus { 381 | outline: none; 382 | border-color: rgba(82, 168, 236, 0.8); 383 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); 384 | -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); 385 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); 386 | } 387 | 388 | /* buttons */ 389 | .markdown-body button { 390 | display: inline-block; 391 | padding: 4px 14px; 392 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 393 | font-size: 13px; 394 | line-height: 18px; 395 | -webkit-border-radius: 4px; 396 | -moz-border-radius: 4px; 397 | border-radius: 4px; 398 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 399 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 400 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 401 | background-color: #0064cd; 402 | background-repeat: repeat-x; 403 | background-image: -webkit-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd)); /* 旧的 Webkit */ 404 | background-image: -webkit-linear-gradient(to bottom, #049cdb, #0064cd); /* 新的 Webkit */ 405 | background-image: -moz-linear-gradient(to bottom, #049cdb, #0064cd); /* Firefox */ 406 | background-image: -o-linear-gradient(to bottom, #049cdb, #0064cd); /* Opera */ 407 | background-image: -ms-linear-gradient(to bottom, #049cdb, #0064cd); /* Internet Explorer */ 408 | background-image: linear-gradient(to bottom, #049cdb, #0064cd); /* 标准语法 */ 409 | color: #fff; 410 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 411 | border: 1px solid #004b9a; 412 | border-bottom-color: #003f81; 413 | -webkit-transition: 0.1s linear all; 414 | -moz-transition: 0.1s linear all; 415 | transition: 0.1s linear all; 416 | border-color: #0064cd #0064cd #003f81; 417 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 418 | } 419 | 420 | .markdown-body button:hover { 421 | color: #fff; 422 | background-position: 0 -15px; 423 | text-decoration: none; 424 | } 425 | 426 | .markdown-body button:active { 427 | -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); 428 | -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); 429 | box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); 430 | } 431 | 432 | .markdown-body button::-moz-focus-inner { 433 | padding: 0; 434 | border: 0; 435 | } -------------------------------------------------------------------------------- /src/i18n/locales/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "Home": { 3 | "Title": "One-Click Keyframe for JianyingPro", 4 | "Description": "Set keyframes for JianyingPro videos with just one click, and easily add transition animations.", 5 | "Keywords": "Jianying Pro One-Touch Keyframe, Jianying Pro One-Second Batch Keyframing, Jianying Pro Auto Keyframing, Jianying Pro Keyframes.", 6 | "TutorialStep": "Only Three Steps", 7 | "HowUse": "How to Use", 8 | "TutorialStep1Title": "Click the 'Select JianyingPro Draft File' button", 9 | "TutorialStep1Description": "Click the 'Select JianyingPro Draft File' button above to choose your JianyingPro draft file. (Note: Please close JianyingPro before proceeding.)", 10 | "TutorialStep2Title": "Choose your JianyingPro draft file", 11 | "TutorialStep2Description": "Locate your draft folder and select the 'draft_info.json' or 'draft_content.json' file.", 12 | "TutorialStep3Title": "Wait for automatic download to your computer", 13 | "TutorialStep3Description": "After the processing is complete, it will be automatically downloaded to your computer. Replace the corresponding file and reopen JianyingPro.", 14 | "SelectDraft": "Select JianyingPro Draft File", 15 | "JianyingProOneClickKeyframe": "Jianying Pro One-Click Keyframe", 16 | "JianyingProBatchKeyframeinOneSecond": "Jianying Pro Batch Keyframe in One Second", 17 | "JianyingProAutomaticKeyframing": "Jianying Pro Automatic Keyframing", 18 | "Donate": "Donate", 19 | "Home": "Home", 20 | "Copilot": "Copilot", 21 | "Download": "Download", 22 | "Notification": "Notification: Please download the Jianying Pro 5.8.0 version to use the Jianying Pro One-Click Keyframe feature (Since Jianying Pro updated its draft file encryption, only the 5.8.0 version of the client can be used) Download address: https://caiyun.139.com/m/i?2f2Tf9XM1p6b4 Extraction code: 7qbg", 23 | "Changelog": "Changelog", 24 | "NovelHotList": "Novel Hot List" 25 | }, 26 | "Faq": { 27 | "FaqTitle": "Frequently Asked", 28 | "FaqDescription": " ", 29 | "List": [ 30 | { 31 | "Question": "What should I do if the draft clipping file cannot be read?", 32 | "Answer": "Please download the latest version of Jianying Pro 5.8.0 to use the Jianying Pro One-Click Keyframe feature (Since Jianying Pro updated its draft file encryption, only the 5.8.0 version of the client can be used) Download address: https://caiyun.139.com/m/i?2f2Tf9XM1p6b4 Extraction code: 7qbg" 33 | }, 34 | { 35 | "Question": "I have already used the 5.8.0 version of Jianying Pro, but I still cannot read the draft file?", 36 | "Answer": "This is because your existing draft file has been encrypted. Even if you downgrade to the 5.8.0 version, the encrypted draft file cannot be read. The solution is: use the 5.8.0 version to create a new draft file for editing." 37 | }, 38 | { 39 | "Question": "What is the One-Click Keyframe feature in Jianying Pro?", 40 | "Answer": "The One-Click Keyframe feature in Jianying Pro is a video editing tool used to create animation effects or transformation effects. It helps enhance visual effects or emphasize specific points in video clips. Jianying Pro's One-Click Keyframe feature simplifies this complex process by allowing users to select the desired starting and ending positions for the animation, automatically creating keyframes." 41 | }, 42 | { 43 | "Question": "How to use the One-Click Keyframe feature in Jianying Pro?", 44 | "Answer": "Open the video clip you want to edit in the Jianying Pro app, find and click on the keyframe icon at the bottom. Then, set the starting and ending keyframes at specific points in the video. Once done, you can adjust the animation effects between keyframes, such as translation, scaling, and more. Finally, preview and save your changes. That's how you use the One-Click Keyframe feature in Jianying Pro." 45 | }, 46 | { 47 | "Question": "What effects can be created using Jianying Pro's One-Click Keyframe?", 48 | "Answer": "With Jianying Pro's One-Click Keyframe, you can create various animation effects like translation, rotation, scaling, and more. Additionally, you can change opacity as needed or create multiple animation effects within the same video clip. These features give you greater flexibility to enhance the visual effects of your video and improve the audience's viewing experience." 49 | }, 50 | { 51 | "Question": "How to delete keyframes in Jianying Pro?", 52 | "Answer": "If you have created keyframes in Jianying Pro but later find that you don't need them, you can follow these steps to delete keyframes: Open the video editing page, find and click on the keyframe icon at the bottom. Then, locate the keyframe you want to delete and click to select it, and finally, click the delete button." 53 | }, 54 | { 55 | "Question": "What are the limitations of Jianying Pro's One-Click Keyframe feature?", 56 | "Answer": "While Jianying Pro's One-Click Keyframe feature is powerful, it does have some limitations. For example, it may not be suitable for all types of video editing and is more geared towards creating animation effects rather than extensive video editing. Additionally, complex animation effects may require more time and effort to create, especially for beginners." 57 | }, 58 | { 59 | "Question": "Do all versions of Jianying Pro have the One-Click Keyframe feature?", 60 | "Answer": "Jianying Pro is an efficient and practical video editing app that continuously introduces new features to provide users with a better experience. The One-Click Keyframe feature is a recent addition and may not be available in all versions. It is recommended that users keep their app updated to access the latest features and optimizations." 61 | } 62 | ], 63 | "Contact": "Can’t find the answer you’re looking for? Reach out to our" 64 | }, 65 | "Donate": { 66 | "BuyTheAuthorACupOfCoffee": "Buy the Author a Cup of Coffee", 67 | "OneCoffee": "One Coffee", 68 | "OneMilkyTea": "One Milky Tea", 69 | "Custom": "Custom", 70 | "DonateOneCoffeeDescription": "if you like him, please buy him a cup of coffee", 71 | "DonateOneMilkyTeaDescription": "if you like him, please buy him a cup of milky tea", 72 | "DonateCustomDescription": "if you like him, please donate custom amount", 73 | "DonateUnit": "cup", 74 | "DonatePlatformAfdian": "Use Afdian", 75 | "DonatePlatformBuyMeACoffee": "Use BuyMeACoffee" 76 | }, 77 | "Copilot": { 78 | "FastSimpleAndSecure": "Fast, simple, and secure", 79 | "CopilotDescription": "makes everything easier: one-click to complete Keyframe! No more tedious manual efforts—just a simple web interface and a lightweight client to control your files. Plus, with every action backed up, you can rest easy knowing your data is safe.", 80 | "DownloadClient": "Download Copilot", 81 | "GetSourceCode": "Get the source code", 82 | "TutorialStep": "Only Three Steps", 83 | "HowUse": "How to Use", 84 | "TutorialStep1Title": "Download and run the Copilot", 85 | "TutorialStep1Description": "Download the client and open it to start the service.", 86 | "TutorialStep2Title": "Open copilot dashboard", 87 | "TutorialStep2Description": "follow Copilot's prompts to open the dashboard, or Open Copilot's dashboard in the browser: https://keyframeai.top/copilot/dashboard.", 88 | "TutorialStep3Title": "Select a draft and Processing", 89 | "TutorialStep3Description": "Select a draft in the console and click the \"Process Drafts\" button. Copilot will automatically process the draft and generate animation effects." 90 | }, 91 | "CopilotDashboard": { 92 | "CheckingCopilotTips": "Checking whether Copilot is started...", 93 | "CheckingCopilotHelpTips": "Can't load? click here", 94 | "DownloadClient": "Download Copilot", 95 | "SelectDraft": "Select a draft", 96 | "UnselectedDraftTitle": "Please select a draft file", 97 | "AddSuccessTitle": "Add successfully", 98 | "AddSuccessText": "Please open the draft in jianyingPro to see the effect", 99 | "AddButton": "Process Drafts", 100 | "WarningNotice": "Warning:", 101 | "WarningNoticeText": "The current client version is in the public test phase, and there may be some problems. If you encounter any problems, please contact us. You can contact us through the online customer service or E-mail: feedback@keyframeai.top" 102 | }, 103 | "CopilotDwonload": { 104 | "Download": "Download" 105 | }, 106 | "DraftSetting": { 107 | "Auto": "Auto", 108 | "DraftSetting": "DraftSetting", 109 | "VideoRatio": "Video ratio (default follows the ratio set in the cutscene)", 110 | "keyframeSpeed": "Keyframe speed (recommended range 1-10)", 111 | "inKeyframeType": "Select the keyframe type (unchecked no keyframes will be added)", 112 | "isRandomInAnimation": "Turn on the random entry animation", 113 | "isClearKeyframes": "Enable cleanup of old keyframes (will delete the original keyframe data and regenerate it)", 114 | "inAnimation": "Select Entry Animation", 115 | "isInAnimation": "Whether to add an entrance animation", 116 | "isClearAnimations": "Enable cleanup of animations", 117 | "PleaseSelectInAnimation": "Please select the entrance animation", 118 | "inAnimationSpeed": "Entry animation speed (in milliseconds 1s = 1000ms)", 119 | "LargeToSmall": "Large=>Small", 120 | "SmallToLarge": "Small=>Large", 121 | "LeftToRight": "Left=>Right", 122 | "RightToLeft": "Right=>Left", 123 | "TopToBottom": "Top=>Bottom", 124 | "BottomToTop": "Bottom=>Top" 125 | }, 126 | "Footer": { 127 | "about": "About", 128 | "followUs": "Follow Us", 129 | "privacyPolicy": "Privacy", 130 | "termsConditions": "Terms", 131 | "legal": "Legal", 132 | "Roadmap": "Roadmap", 133 | "Donate": "Donate" 134 | }, 135 | "NovelList": { 136 | "Link": "Link", 137 | "NovelListTitle": "Novel Hot List", 138 | "NovelListDescription": "This feature is currently being tested", 139 | "Nickname": "Nickname", 140 | "Description": "Description", 141 | "Like": "Like", 142 | "Comment": "Comment", 143 | "Collect": "Collect", 144 | "Share": "Share", 145 | "Action": "Action", 146 | "Show More": "Show More", 147 | "Detail": "Detail", 148 | "CreateTime": "Created Time", 149 | "Hot Index": "Hot Index" 150 | }, 151 | "Leaderboard": { 152 | "pageTitle": "Novel Rankings", 153 | "pageDescription": "Discover the most popular novels across different categories", 154 | "hotRanking": "Hot Ranking", 155 | "likesRanking": "Likes Ranking", 156 | "commentsRanking": "Comments Ranking", 157 | "favoritesRanking": "Favorites Ranking", 158 | "sharesRanking": "Shares Ranking", 159 | "latestRanking": "Latest Ranking", 160 | "nickname": "Nickname", 161 | "description": "Description", 162 | "hotIndex": "Hot Index", 163 | "action": "Action", 164 | "link": "Link", 165 | "detail": "Detail", 166 | "timeRange": "Time Range", 167 | "last7Days": "Last 7 Days", 168 | "last14Days": "Last 14 Days", 169 | "last30Days": "Last 30 Days", 170 | "likes": "Likes", 171 | "comments": "Comments", 172 | "collects": "Collects", 173 | "shares": "Shares", 174 | "publishTime": "Publish Time", 175 | "lastUpdateTime": "Last Data Update Time", 176 | "viewFullRanking": "View Full Ranking", 177 | "fullRankingComingSoon": "Full ranking feature is under development", 178 | "invalidRankingType": "Invalid ranking type", 179 | "backToLeaderboard": "Back to Leaderboard", 180 | "ranking": "Ranking", 181 | "details": "Details", 182 | "topicDetails": { 183 | "backToLeaderboard": "Back to Leaderboard", 184 | "published": "Published", 185 | "likes": "Likes", 186 | "comments": "Comments", 187 | "collects": "Collects", 188 | "shares": "Shares", 189 | "hotIndex": "Hot Index", 190 | "topicId": "Topic ID", 191 | "link": "Link", 192 | "lastUpdated": "Last updated", 193 | "backToPrevious": "Back", 194 | "author": "Author" 195 | } 196 | }, 197 | "Pagination": { 198 | "previous": "Previous", 199 | "next": "Next" 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/app/api/generate/route.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { NextResponse } from "next/server"; 3 | import type { NextRequest } from "next/server"; 4 | import { v4 as uuidv4 } from "uuid"; 5 | import { InAnimations } from "@/jianying/effects/animations"; 6 | import { Transitions } from "@/jianying/effects/transitions"; 7 | 8 | export async function POST(request: NextRequest) { 9 | const chunks: Uint8Array[] = []; 10 | const reader = request.body?.getReader(); 11 | try { 12 | while (true) { 13 | const { done, value } = await reader?.read(); 14 | if (done) break; 15 | chunks.push(value); 16 | } 17 | 18 | const mergedChunk = new Uint8Array( 19 | chunks.reduce((acc, curr) => [...acc, ...curr], []) 20 | ); 21 | // console.log("mergedChunk", mergedChunk) 22 | const requestBody = new TextDecoder("utf-8").decode(mergedChunk); 23 | console.log("requestBody", requestBody); 24 | 25 | // Draft processor data 26 | const draftProcessorData = JSON.parse(requestBody); 27 | // console.log(draftProcessorData) 28 | 29 | const generateDraft = handleDraft( 30 | draftProcessorData.draft, 31 | draftProcessorData.options 32 | ); 33 | // console.log("generateDraft", generateDraft); 34 | return NextResponse.json(generateDraft, { 35 | status: 200, 36 | }); 37 | } catch (error) { 38 | return NextResponse.json( 39 | { 40 | errMsg: error.message, 41 | }, 42 | { 43 | status: 500, 44 | } 45 | ); 46 | } 47 | } 48 | 49 | function handleDraft(data, options) { 50 | // 视频比例设置 51 | if (options && options.videoRatio && options.videoRatio.value !== "auto") { 52 | data.canvas_config.width = options.videoRatio.width; 53 | data.canvas_config.height = options.videoRatio.height; 54 | data.canvas_config.ratio = options.videoRatio.value; 55 | } 56 | 57 | for (let i = 0; i < data.tracks[0].segments.length; i++) { 58 | // 获取主轴上的视频片段 59 | let currentSegments = data.tracks[0].segments[i]; 60 | 61 | // 画布大小 62 | let canvasWidth = data.canvas_config.width; 63 | let canvasHeight = data.canvas_config.height; 64 | 65 | // 获取素材宽高 66 | let videoWidth = data.materials.videos[i].width; 67 | let videoHeight = data.materials.videos[i].height; 68 | 69 | // 获取视频长度 70 | let duration = data.tracks[0].segments[i].source_timerange.duration; 71 | console.log("duration", duration); 72 | 73 | // 放大倍速 74 | let scaleRatio = calculateScale( 75 | canvasWidth, 76 | canvasHeight, 77 | videoWidth, 78 | videoHeight 79 | ); 80 | let scaleBase = 1.3; // 关键帧缩放基础倍数(前端展示名字是 关键帧速度) 81 | if (options && options.keyframeSpeed) { 82 | scaleBase = 1 + Number(options.keyframeSpeed) / 10; 83 | } 84 | console.log("scaleBase", scaleBase); 85 | currentSegments.clip.scale.x = scaleRatio * scaleBase; 86 | currentSegments.clip.scale.y = scaleRatio * scaleBase; 87 | 88 | // 计算缩放后的宽高 89 | let scaleWidth = canvasWidth * scaleBase; 90 | let scaleHeight = canvasHeight * scaleBase; 91 | 92 | //+----------------------- 93 | // 添加 position 关键帧 94 | //+----------------------- 95 | // 计算关键帧需要用到的xy偏移数据 96 | let x_left = (scaleWidth - canvasWidth) / canvasWidth; 97 | let x_right = -x_left; 98 | let y_top = -(scaleHeight - canvasHeight) / canvasHeight; 99 | let y_bottom = -y_top; 100 | 101 | // print data 102 | // console.log("scaleWidth", scaleWidth); 103 | // console.log("scaleHeight", scaleHeight); 104 | // console.log("videoWidth", videoWidth); 105 | // console.log("videoHeight", videoHeight); 106 | // console.log("canvasWidth", canvasWidth); 107 | // console.log("canvasHeight", canvasHeight); 108 | // console.log("x_left", x_left); 109 | // console.log("x_right", x_right); 110 | // console.log("y_top", y_top); 111 | // console.log("y_bottom", y_bottom); 112 | 113 | // 生成xy关键帧数据 114 | let KFTypePositionData = generateKeyFrames( 115 | x_left, 116 | x_right, 117 | y_top, 118 | y_bottom, 119 | duration, 120 | scaleRatio, 121 | scaleBase, 122 | options.inKeyframeTypeCheckedList 123 | ); 124 | 125 | // 组合关键帧数据 126 | // 移除原来所有的关键帧数据 127 | if (options.isClearKeyframes) { 128 | currentSegments.common_keyframes = []; 129 | } 130 | // 遍历所有关键帧数据 131 | KFTypePositionData.forEach((data) => { 132 | currentSegments.common_keyframes.push(data); 133 | }); 134 | 135 | //+----------------------- 136 | // 添加 Transition 关键帧 137 | //+----------------------- 138 | // let transitionData = getTransition(11387229); 139 | // transitionData.id; 140 | 141 | // 添加到素材中 142 | // data.materials.transitions.push(transitionData); 143 | 144 | // 将转场添加到视频片段中 145 | // currentSegments.extra_material_refs.splice(1, 0, transitionData.id); 146 | 147 | //+----------------------- 148 | // 添加入场 material_animations 关键帧 149 | //+----------------------- 150 | // 是否添加入场动画 151 | if (options && options.isInAnimation) { 152 | let inAnimation; 153 | 154 | if (options.isRandomInAnimation) { 155 | const whiteList = options.inAnimationCheckedList ?? []; 156 | inAnimation = getRandomMaterialAnimations(whiteList); 157 | } else if (options.inAnimation) { 158 | inAnimation = getMaterialAnimations(options.inAnimation); 159 | } else { 160 | throw new Error("缺少入场动画参数"); 161 | } 162 | 163 | // 如果有设置入场动画速度,则使用设置的入场动画速度 164 | if (options.inAnimationSpeed) { 165 | inAnimation.duration = Number(options.inAnimationSpeed) * 1000; 166 | } 167 | 168 | // 添加到素材中 169 | // 初始化结构 170 | data.materials.material_animations[i] = getMaterialAnimationLayout(); 171 | // console.log("data.materials.material_animations[i]", data.materials.material_animations[i]); 172 | 173 | data.materials.material_animations[i].animations.push(inAnimation); 174 | // console.log("data.materials.material_animations[i].animations.push", data.materials.material_animations[i]); 175 | 176 | currentSegments.extra_material_refs.splice( 177 | 1, 178 | 0, 179 | data.materials.material_animations[i].id 180 | ); 181 | } else if (options && options.isClearAnimations) { 182 | data.materials.material_animations[i] = []; 183 | } 184 | } 185 | 186 | return data; 187 | } 188 | 189 | // 获取关键帧基本结构 190 | function getKeyFrames(type) { 191 | // KFTypePositionY Y轴 192 | // KFTypePositionX X轴 193 | // KFTypeScaleX X轴缩放 194 | return { 195 | id: uuidv4().toLocaleUpperCase(), 196 | keyframe_list: [], 197 | material_id: "", 198 | property_type: type, 199 | }; 200 | } 201 | 202 | // 获取 xy 坐标的关键帧配置数据 203 | function getKeyFramesDotForPosition(time_offset, value) { 204 | return { 205 | curveType: "Line", 206 | graphID: "", 207 | id: uuidv4().toLocaleUpperCase(), 208 | left_control: { 209 | x: 0, 210 | y: 0, 211 | }, 212 | right_control: { 213 | x: 0, 214 | y: 0, 215 | }, 216 | time_offset: time_offset, 217 | values: [value], 218 | }; 219 | } 220 | 221 | function generateKeyFrames( 222 | xLeft, 223 | xRight, 224 | yTop, 225 | yBottom, 226 | timeOffset, 227 | scaleRatio, 228 | scaleBase, 229 | directionOptions 230 | ) { 231 | const keyframeTypes = { 232 | scaleDown: { 233 | x: [ 234 | { timeOffset: 0, value: 0 }, 235 | { timeOffset: timeOffset, value: 0 }, 236 | ], 237 | y: [ 238 | { timeOffset: 0, value: 0 }, 239 | { timeOffset: timeOffset, value: 0 }, 240 | ], 241 | scale: [ 242 | { timeOffset: 0, value: scaleRatio * scaleBase }, 243 | { timeOffset: timeOffset, value: scaleRatio }, 244 | ], 245 | }, 246 | scaleUp: { 247 | x: [ 248 | { timeOffset: 0, value: 0 }, 249 | { timeOffset: timeOffset, value: 0 }, 250 | ], 251 | y: [ 252 | { timeOffset: 0, value: 0 }, 253 | { timeOffset: timeOffset, value: 0 }, 254 | ], 255 | scale: [ 256 | { timeOffset: 0, value: scaleRatio }, 257 | { timeOffset: timeOffset, value: scaleRatio * scaleBase }, 258 | ], 259 | }, 260 | leftToRight: { 261 | x: [ 262 | { timeOffset: 0, value: xLeft }, 263 | { timeOffset: timeOffset, value: xRight }, 264 | ], 265 | y: [ 266 | { timeOffset: 0, value: 0 }, 267 | { timeOffset: timeOffset, value: 0 }, 268 | ], 269 | }, 270 | rightToLeft: { 271 | x: [ 272 | { timeOffset: 0, value: xRight }, 273 | { timeOffset: timeOffset, value: xLeft }, 274 | ], 275 | y: [ 276 | { timeOffset: 0, value: 0 }, 277 | { timeOffset: timeOffset, value: 0 }, 278 | ], 279 | }, 280 | topToBottom: { 281 | x: [ 282 | { timeOffset: 0, value: 0 }, 283 | { timeOffset: timeOffset, value: 0 }, 284 | ], 285 | y: [ 286 | { timeOffset: 0, value: yTop }, 287 | { timeOffset: timeOffset, value: yBottom }, 288 | ], 289 | }, 290 | bottomToTop: { 291 | x: [ 292 | { timeOffset: 0, value: 0 }, 293 | { timeOffset: timeOffset, value: 0 }, 294 | ], 295 | y: [ 296 | { timeOffset: 0, value: yBottom }, 297 | { timeOffset: timeOffset, value: yTop }, 298 | ], 299 | }, 300 | }; 301 | 302 | if (directionOptions == undefined) { 303 | directionOptions = Object.keys(keyframeTypes); 304 | } 305 | const randomIndex = Math.floor(Math.random() * directionOptions.length); 306 | const selectedType = keyframeTypes[directionOptions[randomIndex]]; 307 | const selectedKey = directionOptions[randomIndex]; 308 | 309 | if (["scaleUp", "scaleDown"].includes(selectedKey)) { 310 | const KFTypeScale = getKeyFrames("KFTypeScaleX"); 311 | const scaleStartKey = getKeyFramesDotForPosition( 312 | selectedType.scale[0].timeOffset, 313 | selectedType.scale[0].value 314 | ); 315 | const scaleEndKey = getKeyFramesDotForPosition( 316 | selectedType.scale[1].timeOffset, 317 | selectedType.scale[1].value 318 | ); 319 | KFTypeScale.keyframe_list.push(scaleStartKey); 320 | KFTypeScale.keyframe_list.push(scaleEndKey); 321 | return [KFTypeScale]; 322 | } else if ( 323 | ["leftToRight", "rightToLeft", "topToBottom", "bottomToTop"].includes( 324 | selectedKey 325 | ) 326 | ) { 327 | const KFTypePositionX = getKeyFrames("KFTypePositionX"); 328 | const KFTypePositionY = getKeyFrames("KFTypePositionY"); 329 | 330 | const xStart = getKeyFramesDotForPosition( 331 | selectedType.x[0].timeOffset, 332 | selectedType.x[0].value 333 | ); 334 | const xEnd = getKeyFramesDotForPosition( 335 | selectedType.x[1].timeOffset, 336 | selectedType.x[1].value 337 | ); 338 | const yStart = getKeyFramesDotForPosition( 339 | selectedType.y[0].timeOffset, 340 | selectedType.y[0].value 341 | ); 342 | const yEnd = getKeyFramesDotForPosition( 343 | selectedType.y[1].timeOffset, 344 | selectedType.y[1].value 345 | ); 346 | 347 | KFTypePositionX.keyframe_list.push(xStart); 348 | KFTypePositionX.keyframe_list.push(xEnd); 349 | KFTypePositionY.keyframe_list.push(yStart); 350 | KFTypePositionY.keyframe_list.push(yEnd); 351 | return [KFTypePositionX, KFTypePositionY]; 352 | } else { 353 | return []; 354 | } 355 | } 356 | 357 | // 随机数 358 | function getRandomInt(min, max) { 359 | min = Math.ceil(min); 360 | max = Math.floor(max); 361 | return Math.floor(Math.random() * (max - min + 1)) + min; 362 | } 363 | 364 | // 获取转场 365 | function getTransition(effect_id) { 366 | let transitionList = Transitions; 367 | return transitionList[effect_id]; 368 | } 369 | 370 | /** 371 | * 计算缩放比例倍 372 | * @param {number} targetWidth - 目标宽度 373 | * @param {number} targetHeight - 目标高度 374 | * @param {number} sourceWidth - 素材宽度 375 | * @param {number} sourceHeight - 素材高度 376 | * @returns {number} 缩放比例 377 | */ 378 | function calculateScale(targetWidth, targetHeight, sourceWidth, sourceHeight) { 379 | let scaledWidth, scaledHeight; 380 | let scale; 381 | 382 | // 判断目标的宽高哪个是短边 383 | if (targetHeight < targetWidth) { 384 | // 高度是短边,等比例缩放素材到目标高度 385 | scaledWidth = (targetHeight / sourceHeight) * sourceWidth; 386 | scaledHeight = targetHeight; 387 | } else { 388 | // 宽度是短边,等比例缩放素材到目标宽度 389 | scaledHeight = (targetWidth / sourceWidth) * sourceHeight; 390 | scaledWidth = targetWidth; 391 | } 392 | 393 | // 根据短边计算宽或高的放大比例,进1取整并保留一位小数 394 | if (scaledWidth < targetWidth) { 395 | // 如果宽是短边,则计算宽度的放大比例 396 | scale = Math.ceil((targetWidth / scaledWidth) * 10) / 10; 397 | } else { 398 | // 如果高是短边,则计算高度的放大比例 399 | scale = Math.ceil((targetHeight / scaledHeight) * 10) / 10; 400 | } 401 | 402 | return scale; 403 | } 404 | 405 | // 示例调用 406 | // console.log(calculateScale(500, 300, 400, 200)); // 输出类似:1.7 407 | 408 | // 获取动画结构 409 | function getMaterialAnimationLayout() { 410 | return { 411 | animations: [], 412 | id: uuidv4().toLocaleUpperCase(), 413 | multi_language_current: "none", 414 | type: "sticker_animation", 415 | }; 416 | } 417 | 418 | // 获取入场动画 419 | function getMaterialAnimations(effect_id) { 420 | return InAnimations[effect_id]; 421 | } 422 | 423 | // 随机获取入场动画 424 | function getRandomMaterialAnimations(whiteList: string[] | undefined) { 425 | const InAnimationsArr = Object.values(InAnimations); 426 | const filteredInAnimationsArr = InAnimationsArr.filter((animation) => { 427 | if (whiteList?.length) { 428 | return whiteList.indexOf(animation.id) != -1; 429 | } else { 430 | return true; 431 | } 432 | }); 433 | let randomAnimations = 434 | filteredInAnimationsArr[ 435 | Math.floor(Math.random() * filteredInAnimationsArr.length) 436 | ]; 437 | return randomAnimations; 438 | } 439 | -------------------------------------------------------------------------------- /src/app/[locale]/components/DraftSetting.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | ToggleSwitch, 5 | Label, 6 | Modal, 7 | TextInput, 8 | Select, 9 | Checkbox, 10 | Radio, 11 | } from "flowbite-react"; 12 | import { useEffect, useState } from "react"; 13 | import { InAnimations } from "@/jianying/effects/animations"; 14 | import { useTranslations } from "next-intl"; 15 | 16 | export default function DraftSetting() { 17 | const t = useTranslations("DraftSetting"); 18 | 19 | const [openModal, setOpenModal] = useState(false); 20 | 21 | function onCloseModal() { 22 | setOpenModal(false); 23 | } 24 | 25 | const videoRatioOptions = [ 26 | { id: "ratio_auto", value: "auto", label: t("Auto") }, 27 | { 28 | id: "ratio_16_9", 29 | value: "16:9", 30 | label: "16:9", 31 | width: 1920, 32 | height: 1080, 33 | }, 34 | { 35 | id: "ratio_9_16", 36 | value: "9:16", 37 | label: "9:16", 38 | width: 1080, 39 | height: 1920, 40 | }, 41 | { id: "ratio_4_3", value: "4:3", label: "4:3", width: 1920, height: 1440 }, 42 | ]; 43 | 44 | 45 | const [videoRatio, setVideoRatio] = useState("auto"); 46 | useEffect(() => { 47 | // 只在浏览器端访问 localStorage 48 | const storedVideoRatio = localStorage.getItem("videoRatio"); 49 | if (storedVideoRatio) { 50 | setVideoRatio(storedVideoRatio); 51 | } 52 | }, []); 53 | 54 | const videoRatioCheckedChangeHandle = ( 55 | event: React.ChangeEvent 56 | ) => { 57 | const newRatio = event.target.value; 58 | setVideoRatio(newRatio); 59 | }; 60 | 61 | 62 | const [keyframeSpeed, setKeyframeSpeed] = useState(3); 63 | useEffect(() => { 64 | // 只在浏览器端访问 localStorage 65 | const storedKeyframeSpeed = localStorage.getItem("keyframeSpeed"); 66 | if (storedKeyframeSpeed) { 67 | setKeyframeSpeed(parseInt(storedKeyframeSpeed)); 68 | } 69 | }, []); 70 | 71 | // 关键帧类型 72 | const inKeyframeTypeOptions = [{ value: "scaleDown", label: t("LargeToSmall") }, { value: "scaleUp", label: t("SmallToLarge") }, { value: "leftToRight", label: t("LeftToRight") }, { value: "rightToLeft", label: t("RightToLeft") }, { value: "topToBottom", label: t("TopToBottom") }, { value: "bottomToTop", label: t("BottomToTop") }]; 73 | const [inKeyframeTypeCheckedList, setInKeyframeTypeCheckedList] = useState( 74 | [] 75 | ); 76 | useEffect(() => { 77 | const defaultCheckedList = ["scaleDown", "scaleUp", "leftToRight", "rightToLeft", "topToBottom", "bottomToTop"]; 78 | const storedCheckedList = localStorage.getItem("inKeyframeTypeCheckedList"); 79 | if (storedCheckedList) { 80 | setInKeyframeTypeCheckedList(JSON.parse(storedCheckedList)); 81 | } else { 82 | setInKeyframeTypeCheckedList(defaultCheckedList); 83 | } 84 | }, []); // 空数组确保只在组件挂载时执行 85 | 86 | const inKeyframeTypeCheckedChangeHandle = ( 87 | event: React.ChangeEvent 88 | ) => { 89 | const checkedList = [...inKeyframeTypeCheckedList]; 90 | if (event.target.checked) { 91 | checkedList.push(event.target.value); 92 | } else { 93 | checkedList.splice(checkedList.indexOf(event.target.value), 1); 94 | } 95 | setInKeyframeTypeCheckedList(checkedList); 96 | 97 | // 更新 localStorage 98 | localStorage.setItem("inKeyframeTypeCheckedList", JSON.stringify(checkedList)); 99 | }; 100 | 101 | const [isClearKeyframes, setIsClearKeyframes] = useState(true); 102 | useEffect(() => { 103 | // 只在浏览器端访问 localStorage 104 | const storedIsClearKeyframes = localStorage.getItem("isClearKeyframes"); 105 | if (storedIsClearKeyframes) { 106 | setIsClearKeyframes(storedIsClearKeyframes === "false" ? false : true); 107 | } 108 | }, []); 109 | 110 | // 入场动画开关 111 | const [isInAnimation, setIsInAnimation] = useState(true); 112 | useEffect(() => { 113 | // 只在浏览器端访问 localStorage 114 | const storedIsInAnimation = localStorage.getItem("isInAnimation"); 115 | if (storedIsInAnimation) { 116 | setIsInAnimation(storedIsInAnimation === "false" ? false : true); 117 | } 118 | }, []); 119 | 120 | // 是否清理动画 121 | const [isClearAnimations, setIsClearAnimations] = useState(true); 122 | useEffect(() => { 123 | // 只在浏览器端访问 localStorage 124 | const storedIsClearAnimations = localStorage.getItem("isClearAnimations"); 125 | if (storedIsClearAnimations) { 126 | setIsClearAnimations(storedIsClearAnimations === "false" ? false : true); 127 | } 128 | }, []); 129 | 130 | // 随机入场动画开关 131 | const [isRandomInAnimation, setIsRandomInAnimation] = useState(true); 132 | useEffect(() => { 133 | // 只在浏览器端访问 localStorage 134 | const storedIsRandomInAnimation = localStorage.getItem("isRandomInAnimation"); 135 | if (storedIsRandomInAnimation) { 136 | setIsRandomInAnimation(storedIsRandomInAnimation === "false" ? false : true); 137 | } 138 | }, []); 139 | 140 | 141 | const inAnimationsOptions = Object.values(InAnimations); 142 | const [inAnimation, setInAnimation] = useState(""); 143 | const [inAnimationSpeed, setInAnimationSpeed] = useState(500); 144 | useEffect(() => { 145 | // 只在浏览器端访问 localStorage 146 | const storedInAnimation = localStorage.getItem("inAnimation"); 147 | if (storedInAnimation) { 148 | setInAnimation(storedInAnimation); 149 | } 150 | }, []); 151 | useEffect(() => { 152 | // 只在浏览器端访问 localStorage 153 | const storedInAnimationSpeed = localStorage.getItem("inAnimationSpeed"); 154 | if (storedInAnimationSpeed) { 155 | setInAnimationSpeed(parseInt(storedInAnimationSpeed)); 156 | } 157 | }, []); 158 | 159 | const [inAnimationCheckedList, setInAnimationCheckedList] = useState( 160 | [] 161 | ); 162 | 163 | useEffect(() => { 164 | const defaultCheckedList = Object.keys(InAnimations); 165 | const storedCheckedList = localStorage.getItem("inAnimationCheckedList"); 166 | if (storedCheckedList) { 167 | setInAnimationCheckedList(JSON.parse(storedCheckedList)); 168 | } else { 169 | setInAnimationCheckedList(defaultCheckedList); 170 | } 171 | }, []); // 空数组确保只在组件挂载时执行 172 | 173 | const inAnimationCheckedChangeHandle = ( 174 | event: React.ChangeEvent 175 | ) => { 176 | const checkedList = [...inAnimationCheckedList]; 177 | if (event.target.checked) { 178 | checkedList.push(event.target.value); 179 | } else { 180 | checkedList.splice(checkedList.indexOf(event.target.value), 1); 181 | } 182 | setInAnimationCheckedList(checkedList); 183 | 184 | // 更新 localStorage 185 | localStorage.setItem("inAnimationCheckedList", JSON.stringify(checkedList)); 186 | }; 187 | 188 | 189 | useEffect(() => { 190 | const timer = setTimeout(() => { 191 | localStorage.setItem("videoRatio", videoRatio); 192 | localStorage.setItem("keyframeSpeed", keyframeSpeed.toString()); 193 | localStorage.setItem("inKeyframeTypeCheckedList", JSON.stringify(inKeyframeTypeCheckedList)); 194 | localStorage.setItem("isClearKeyframes", isClearKeyframes.toString()); 195 | localStorage.setItem("isInAnimation", isInAnimation.toString()); 196 | localStorage.setItem("isClearAnimations", isClearAnimations.toString()); 197 | localStorage.setItem( 198 | "isRandomInAnimation", 199 | isRandomInAnimation.toString() 200 | ); 201 | localStorage.setItem("inAnimation", inAnimation); 202 | localStorage.setItem("inAnimationSpeed", inAnimationSpeed.toString()); 203 | localStorage.setItem( 204 | "inAnimationCheckedList", 205 | JSON.stringify(inAnimationCheckedList) 206 | ); 207 | 208 | localStorage.setItem( 209 | "draftOptions", 210 | JSON.stringify({ 211 | videoRatio: videoRatioOptions.find( 212 | (option) => option.value === videoRatio 213 | ), 214 | keyframeSpeed, 215 | inKeyframeTypeCheckedList, 216 | isClearKeyframes, 217 | isRandomInAnimation, 218 | isInAnimation, 219 | isClearAnimations, 220 | inAnimation, 221 | inAnimationSpeed, 222 | inAnimationCheckedList, 223 | }) 224 | ); 225 | }, 500); 226 | 227 | return () => { 228 | clearInterval(timer); 229 | }; 230 | }, [ 231 | videoRatio, 232 | keyframeSpeed, 233 | inKeyframeTypeCheckedList, 234 | isClearKeyframes, 235 | isInAnimation, 236 | isClearAnimations, 237 | isRandomInAnimation, 238 | inAnimation, 239 | inAnimationSpeed, 240 | inAnimationCheckedList, 241 | ]); 242 | 243 | return ( 244 | <> 245 |
246 | setOpenModal(true)} 249 | className="font-medium text-blue-600 dark:text-blue-500 hover:underline" 250 | > 251 | {t("DraftSetting")} 252 | 253 | 254 | 255 | 256 |
257 |

258 | {t("DraftSetting")} 259 |

260 | 261 | {/* 视频比例 */} 262 |
263 |
264 |
266 |
267 | {videoRatioOptions.map((option) => ( 268 |
269 | 276 | 277 |
278 | ))} 279 |
280 |
281 | 282 | {/* 关键帧速度 */} 283 |
284 |
285 |
290 | ) => 295 | setKeyframeSpeed(e.target.value as any) 296 | } 297 | /> 298 |
299 | 300 | {/* 选择关键帧类型,至少选择一个 */} 301 |
302 |
303 |
308 |
309 | {inKeyframeTypeOptions.map((option) => ( 310 |
314 | 320 | 326 |
327 | ))} 328 |
329 |
330 | 331 | {/* 清理旧关键帧开关 */} 332 |
333 |
334 |
339 | 343 |
344 | 345 | {/* 入场动画开关 */} 346 |
347 |
348 |
353 | 357 |
358 | 359 | {/* 清理动画开关 */} 360 | {!isInAnimation && ( 361 |
362 |
363 |
368 | 372 |
373 | )} 374 | 375 | {/* 随机入场动画开关 */} 376 | {isInAnimation && ( 377 |
378 |
379 |
384 | 388 |
389 | )} 390 | 391 | {/* 入场动画选择,如果是随机入场动画,则不显示 */} 392 | {!isRandomInAnimation && isInAnimation ? ( 393 |
394 |
395 |
397 | 411 |
412 | ) : null} 413 | 414 | {isRandomInAnimation && isInAnimation ? ( 415 |
416 |
417 |
419 |
420 | {inAnimationsOptions.map((option) => ( 421 |
425 | 433 | 439 |
440 | ))} 441 |
442 |
443 | ) : null} 444 | 445 | {/* 入场动画速度 */} 446 | {isInAnimation ? ( 447 |
448 |
449 |
454 | ) => 459 | setInAnimationSpeed(e.target.value as any) 460 | } 461 | /> 462 |
463 | ) : null} 464 |
465 |
466 |
467 |
468 | 469 | ); 470 | } 471 | -------------------------------------------------------------------------------- /src/app/[locale]/copilot/dashboard/dashboard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslations } from "next-intl"; 3 | import { useEffect, useState, useRef } from "react"; 4 | import Swal from "sweetalert2"; 5 | import { 6 | ToggleSwitch, 7 | Label, 8 | TextInput, 9 | Select, 10 | Checkbox, 11 | Radio, 12 | Spinner, 13 | } from "flowbite-react"; 14 | import { InAnimations } from "@/jianying/effects/animations"; 15 | 16 | export default function DashboardComponent() { 17 | const t = useTranslations("CopilotDashboard"); 18 | const tds = useTranslations("DraftSetting"); 19 | 20 | 21 | // 获取草稿列表 22 | const [drafts, setDrafts]: any = useState([]); 23 | useEffect(() => { 24 | const fetchDrafts = async () => { 25 | const response = await fetch( 26 | window.localStorage.getItem("copilot_api_url") + "/api/v1/drafts" 27 | ); 28 | const data = await response.json(); 29 | console.log("fetchDrafts", data); 30 | setDrafts(data.data); 31 | }; 32 | 33 | const timer = setTimeout(() => { 34 | fetchDrafts(); 35 | }, 200); 36 | 37 | return () => { 38 | clearInterval(timer); 39 | }; 40 | }, []); 41 | 42 | const videoRatioOptions = [ 43 | { id: "ratio_auto", value: "auto", label: tds("Auto") }, 44 | { 45 | id: "ratio_16_9", 46 | value: "16:9", 47 | label: "16:9", 48 | width: 1920, 49 | height: 1080, 50 | }, 51 | { 52 | id: "ratio_9_16", 53 | value: "9:16", 54 | label: "9:16", 55 | width: 1080, 56 | height: 1920, 57 | }, 58 | { id: "ratio_4_3", value: "4:3", label: "4:3", width: 1920, height: 1440 }, 59 | ]; 60 | const [videoRatio, setVideoRatio] = useState("auto"); 61 | useEffect(() => { 62 | // 只在浏览器端访问 localStorage 63 | const storedVideoRatio = localStorage.getItem("videoRatio"); 64 | if (storedVideoRatio) { 65 | setVideoRatio(storedVideoRatio); 66 | } 67 | }, []); 68 | 69 | const videoRatioCheckedChangeHandle = ( 70 | event: React.ChangeEvent 71 | ) => { 72 | const newRatio = event.target.value; 73 | setVideoRatio(newRatio); 74 | }; 75 | 76 | const [keyframeSpeed, setKeyframeSpeed] = useState(3); 77 | useEffect(() => { 78 | // 只在浏览器端访问 localStorage 79 | const storedKeyframeSpeed = localStorage.getItem("keyframeSpeed"); 80 | if (storedKeyframeSpeed) { 81 | setKeyframeSpeed(parseInt(storedKeyframeSpeed)); 82 | } 83 | }, []); 84 | 85 | // 关键帧类型 86 | const inKeyframeTypeOptions = [{ value: "scaleDown", label: tds("LargeToSmall") }, { value: "scaleUp", label: tds("SmallToLarge") }, { value: "leftToRight", label: tds("LeftToRight") }, { value: "rightToLeft", label: tds("RightToLeft") }, { value: "topToBottom", label: tds("TopToBottom") }, { value: "bottomToTop", label: tds("BottomToTop") }]; 87 | const [inKeyframeTypeCheckedList, setInKeyframeTypeCheckedList] = useState( 88 | [] 89 | ); 90 | useEffect(() => { 91 | const defaultCheckedList = ["scaleDown", "scaleUp", "leftToRight", "rightToLeft", "topToBottom", "bottomToTop"]; 92 | const storedCheckedList = localStorage.getItem("inKeyframeTypeCheckedList"); 93 | if (storedCheckedList) { 94 | setInKeyframeTypeCheckedList(JSON.parse(storedCheckedList)); 95 | } else { 96 | setInKeyframeTypeCheckedList(defaultCheckedList); 97 | } 98 | }, []); // 空数组确保只在组件挂载时执行 99 | 100 | const inKeyframeTypeCheckedChangeHandle = ( 101 | event: React.ChangeEvent 102 | ) => { 103 | const checkedList = [...inKeyframeTypeCheckedList]; 104 | if (event.target.checked) { 105 | checkedList.push(event.target.value); 106 | } else { 107 | checkedList.splice(checkedList.indexOf(event.target.value), 1); 108 | } 109 | setInKeyframeTypeCheckedList(checkedList); 110 | 111 | // 更新 localStorage 112 | localStorage.setItem("inKeyframeTypeCheckedList", JSON.stringify(checkedList)); 113 | }; 114 | 115 | const [isClearKeyframes, setIsClearKeyframes] = useState(true); 116 | useEffect(() => { 117 | // 只在浏览器端访问 localStorage 118 | const storedIsClearKeyframes = localStorage.getItem("isClearKeyframes"); 119 | if (storedIsClearKeyframes) { 120 | setIsClearKeyframes(storedIsClearKeyframes === "false" ? false : true); 121 | } 122 | }, []); 123 | 124 | // 入场动画开关 125 | const [isInAnimation, setIsInAnimation] = useState(true); 126 | useEffect(() => { 127 | // 只在浏览器端访问 localStorage 128 | const storedIsInAnimation = localStorage.getItem("isInAnimation"); 129 | if (storedIsInAnimation) { 130 | setIsInAnimation(storedIsInAnimation === "false" ? false : true); 131 | } 132 | }, []); 133 | 134 | // 是否清理动画 135 | const [isClearAnimations, setIsClearAnimations] = useState(true); 136 | useEffect(() => { 137 | // 只在浏览器端访问 localStorage 138 | const storedIsClearAnimations = localStorage.getItem("isClearAnimations"); 139 | if (storedIsClearAnimations) { 140 | setIsClearAnimations(storedIsClearAnimations === "false" ? false : true); 141 | } 142 | }, []); 143 | 144 | // 随机入场动画开关 145 | const [isRandomInAnimation, setIsRandomInAnimation] = useState(true); 146 | useEffect(() => { 147 | // 只在浏览器端访问 localStorage 148 | const storedIsRandomInAnimation = localStorage.getItem("isRandomInAnimation"); 149 | if (storedIsRandomInAnimation) { 150 | setIsRandomInAnimation(storedIsRandomInAnimation === "false" ? false : true); 151 | } 152 | }, []); 153 | 154 | const inAnimationsOptions = Object.values(InAnimations); 155 | const [inAnimation, setInAnimation] = useState(""); 156 | const [inAnimationSpeed, setInAnimationSpeed] = useState(500); 157 | useEffect(() => { 158 | // 只在浏览器端访问 localStorage 159 | const storedInAnimation = localStorage.getItem("inAnimation"); 160 | if (storedInAnimation) { 161 | setInAnimation(storedInAnimation); 162 | } 163 | }, []); 164 | useEffect(() => { 165 | // 只在浏览器端访问 localStorage 166 | const storedInAnimationSpeed = localStorage.getItem("inAnimationSpeed"); 167 | if (storedInAnimationSpeed) { 168 | setInAnimationSpeed(parseInt(storedInAnimationSpeed)); 169 | } 170 | }, []); 171 | 172 | const [inAnimationCheckedList, setInAnimationCheckedList] = useState( 173 | [] 174 | ); 175 | 176 | useEffect(() => { 177 | const defaultCheckedList = Object.keys(InAnimations); 178 | const storedCheckedList = localStorage.getItem("inAnimationCheckedList"); 179 | if (storedCheckedList) { 180 | setInAnimationCheckedList(JSON.parse(storedCheckedList)); 181 | } else { 182 | setInAnimationCheckedList(defaultCheckedList); 183 | } 184 | }, []); // 空数组确保只在组件挂载时执行 185 | 186 | const inAnimationCheckedChangeHandle = ( 187 | event: React.ChangeEvent 188 | ) => { 189 | const checkedList = [...inAnimationCheckedList]; 190 | if (event.target.checked) { 191 | checkedList.push(event.target.value); 192 | } else { 193 | checkedList.splice(checkedList.indexOf(event.target.value), 1); 194 | } 195 | setInAnimationCheckedList(checkedList); 196 | 197 | // 更新 localStorage 198 | localStorage.setItem("inAnimationCheckedList", JSON.stringify(checkedList)); 199 | }; 200 | 201 | useEffect(() => { 202 | const timer = setTimeout(() => { 203 | localStorage.setItem("videoRatio", videoRatio); 204 | localStorage.setItem("keyframeSpeed", keyframeSpeed.toString()); 205 | localStorage.setItem("inKeyframeTypeCheckedList", JSON.stringify(inKeyframeTypeCheckedList)); 206 | localStorage.setItem("isClearKeyframes", isClearKeyframes.toString()); 207 | localStorage.setItem("isInAnimation", isInAnimation.toString()); 208 | localStorage.setItem("isClearAnimations", isClearAnimations.toString()); 209 | localStorage.setItem( 210 | "isRandomInAnimation", 211 | isRandomInAnimation.toString() 212 | ); 213 | localStorage.setItem("inAnimation", inAnimation); 214 | localStorage.setItem("inAnimationSpeed", inAnimationSpeed.toString()); 215 | localStorage.setItem( 216 | "inAnimationCheckedList", 217 | JSON.stringify(inAnimationCheckedList) 218 | ); 219 | 220 | localStorage.setItem( 221 | "draftOptions", 222 | JSON.stringify({ 223 | videoRatio: videoRatioOptions.find( 224 | (option) => option.value === videoRatio 225 | ), 226 | keyframeSpeed, 227 | inKeyframeTypeCheckedList, 228 | isClearKeyframes, 229 | isClearAnimations, 230 | isRandomInAnimation, 231 | isInAnimation, 232 | inAnimation, 233 | inAnimationSpeed, 234 | inAnimationCheckedList, 235 | }) 236 | ); 237 | }, 500); 238 | 239 | return () => { 240 | clearInterval(timer); 241 | }; 242 | }, [ 243 | videoRatio, 244 | keyframeSpeed, 245 | inKeyframeTypeCheckedList, 246 | isClearKeyframes, 247 | isInAnimation, 248 | isClearAnimations, 249 | isRandomInAnimation, 250 | inAnimation, 251 | inAnimationSpeed, 252 | inAnimationCheckedList, 253 | ]); 254 | 255 | // 获取选中的草稿 256 | const selecDraftRef = useRef(null); 257 | const [selectedDraft, setSelectedDraft] = useState(""); 258 | async function handleSelectDraft() { 259 | const draftJsonFile = selecDraftRef.current?.value; 260 | // console.log("draftJsonFile", draftJsonFile); 261 | if (!draftJsonFile) { 262 | setSelectedDraft(""); 263 | } else { 264 | setSelectedDraft(draftJsonFile); 265 | } 266 | } 267 | 268 | // 一键处理草稿 269 | const [processing, setProcessing] = useState(false); 270 | async function handleProcessDraft( 271 | event: React.MouseEvent 272 | ) { 273 | console.log("handleProcessDraft", event); 274 | event.preventDefault(); 275 | 276 | // 获取选中的草稿内容 277 | const selectedDraft = selecDraftRef.current?.value; 278 | console.log("selectedDraft", selectedDraft); 279 | if (!selectedDraft) { 280 | Swal.fire({ 281 | title: t("UnselectedDraftTitle"), 282 | icon: "warning", 283 | }); 284 | return; 285 | } 286 | 287 | // 如果正在处理,则不重新处理 288 | if (processing) { 289 | return; 290 | } 291 | setProcessing(true); 292 | 293 | const response = await fetch( 294 | window.localStorage.getItem("copilot_api_url") + 295 | `/api/v1/draft?draft_json_file=${selectedDraft}` 296 | ); 297 | const result = await response.json(); 298 | // console.log("handleProcessDraft", result.data.draft_info); 299 | 300 | // 获取草稿处理设置 301 | const options = JSON.parse(localStorage.getItem("draftOptions") || "{}"); 302 | 303 | // 处理草稿 304 | const processedResponse = await fetch( 305 | `/api/generate?filename=draft_info.json`, 306 | { 307 | method: "POST", 308 | body: JSON.stringify({ 309 | options: { 310 | ...options, 311 | }, 312 | draft: result.data.draft_info, 313 | }), 314 | headers: { 315 | "Content-Type": "application/json", 316 | }, 317 | } 318 | ); 319 | 320 | const processedDraft = await processedResponse.json(); 321 | // console.log("processedDraft", processedDraft); 322 | if (processedDraft.errMsg) { 323 | Swal.fire({ 324 | title: "Error", 325 | text: processedDraft.errMsg, 326 | icon: "error", 327 | }); 328 | setProcessing(false); 329 | return; 330 | } 331 | 332 | // 保存处理后的草稿 333 | const saveResponse = await fetch( 334 | window.localStorage.getItem("copilot_api_url") + `/api/v1/draft`, 335 | { 336 | method: "POST", 337 | body: JSON.stringify({ 338 | draft_json_file: selectedDraft, 339 | draft_info: processedDraft, 340 | }), 341 | headers: { 342 | "Content-Type": "application/json", 343 | }, 344 | } 345 | ); 346 | 347 | const saveDraft = await saveResponse.json(); 348 | 349 | Swal.fire({ 350 | title: t("AddSuccessTitle"), 351 | text: t("AddSuccessText"), 352 | icon: "success", 353 | }); 354 | 355 | setProcessing(false); 356 | } 357 | return ( 358 |
359 |
363 | {t("WarningNotice")}{" "} 364 | {t("WarningNoticeText")} 365 |
366 |
367 | 370 | 385 |
386 | 387 |
388 |

{tds("DraftSetting")}

389 | 390 | {/* 视频比例 */} 391 |
392 |
393 |
398 |
399 | {videoRatioOptions.map((option) => ( 400 |
401 | 408 | 411 |
412 | ))} 413 |
414 |
415 | 416 | {/* 关键帧速度 */} 417 |
418 |
419 |
425 | ) => 430 | setKeyframeSpeed(e.target.value as any) 431 | } 432 | /> 433 |
434 | 435 | {/* 选择关键帧类型,至少选择一个 */} 436 |
437 |
438 |
444 |
445 | {inKeyframeTypeOptions.map((option) => ( 446 |
450 | 456 | 462 |
463 | ))} 464 |
465 |
466 | 467 | {/* 清理旧关键帧开关 */} 468 |
469 |
470 |
476 | 480 |
481 | 482 | {/* 入场动画开关 */} 483 |
484 |
485 |
491 | 495 |
496 | 497 | {/* 清理动画开关 */} 498 | {!isInAnimation && ( 499 |
500 |
501 |
507 | 511 |
512 | )} 513 | 514 | {/* 随机入场动画开关 */} 515 | {isInAnimation && ( 516 |
517 |
518 |
524 | 528 |
529 | )} 530 | 531 | {/* 入场动画选择,如果是随机入场动画,则不显示 */} 532 | {!isRandomInAnimation && isInAnimation ? ( 533 |
534 |
535 |
541 | 553 |
554 | ) : null} 555 | 556 | {isRandomInAnimation && isInAnimation ? ( 557 |
558 |
559 |
565 |
566 | {inAnimationsOptions.map((option) => ( 567 |
571 | 577 | 583 |
584 | ))} 585 |
586 |
587 | ) : null} 588 | 589 | {/* 入场动画速度 */} 590 | {isInAnimation ? ( 591 |
592 |
593 |
599 | ) => 604 | setInAnimationSpeed(e.target.value as any) 605 | } 606 | /> 607 |
608 | ) : null} 609 |
610 | 611 | 618 |
619 | ); 620 | } 621 | --------------------------------------------------------------------------------