diff --git a/live-wallpaper-creator/.gitignore b/live-wallpaper-creator/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/live-wallpaper-creator/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/live-wallpaper-creator/README.md b/live-wallpaper-creator/README.md new file mode 100644 index 0000000..40b38e0 --- /dev/null +++ b/live-wallpaper-creator/README.md @@ -0,0 +1,91 @@ +# Live Wallpaper Creator + +A web-based tool for creating animated wallpapers with customizable overlays. Built with React and FFmpeg. + +## Features + +- 🖼️ Upload and manipulate background images +- 🎞️ Export as GIF or MP4 +- ⌨️ Keyboard shortcuts for common actions +- 📊 Progress tracking for exports +- 🎨 Modern dark theme UI +- ♿ Accessibility features + +## Keyboard Shortcuts + +- `Ctrl/⌘ + O`: Open file picker +- `Ctrl/⌘ + S`: Save as GIF +- `Ctrl/⌘ + Shift + S`: Save as MP4 +- `?`: Toggle help dialog + +## Browser Requirements + +- Modern browser (Chrome, Firefox, or Edge) +- HTTPS connection (required for SharedArrayBuffer) +- WebAssembly support + +## Development + +### Prerequisites + +- Node.js 16+ +- npm or yarn + +### Setup + +1. Clone the repository +2. Install dependencies: + ```bash + npm install + ``` +3. Start development server: + ```bash + npm run dev + ``` + +### Building + +```bash +npm run build +``` + +The built files will be in the `dist` directory. + +## Technical Details + +- Built with: + - React + - Material-UI + - FFmpeg.wasm + - Vite + +- Features: + - Client-side video processing + - Drag and drop interface + - Progress tracking + - Accessibility support + - Keyboard shortcuts + +## Browser Support + +The app requires: +- SharedArrayBuffer support +- WebAssembly support +- Secure context (HTTPS) + +For best results, use: +- Chrome (latest) +- Firefox (latest) +- Edge (latest) + +## Contributing + +1. Fork the repository +2. Create your feature branch +3. Commit your changes +4. Push to the branch +5. Create a Pull Request + +## License + +MIT License diff --git a/live-wallpaper-creator/eslint.config.js b/live-wallpaper-creator/eslint.config.js new file mode 100644 index 0000000..238d2e4 --- /dev/null +++ b/live-wallpaper-creator/eslint.config.js @@ -0,0 +1,38 @@ +import js from '@eslint/js' +import globals from 'globals' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + settings: { react: { version: '18.3' } }, + plugins: { + react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + ...reactHooks.configs.recommended.rules, + 'react/jsx-no-target-blank': 'off', + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/live-wallpaper-creator/index.html b/live-wallpaper-creator/index.html new file mode 100644 index 0000000..1322bad --- /dev/null +++ b/live-wallpaper-creator/index.html @@ -0,0 +1,14 @@ + + + + + + + + Live Wallpaper Creator + + + +
+ + diff --git a/live-wallpaper-creator/package.json b/live-wallpaper-creator/package.json new file mode 100644 index 0000000..af84799 --- /dev/null +++ b/live-wallpaper-creator/package.json @@ -0,0 +1,36 @@ +{ + "name": "live-wallpaper-creator", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@ffmpeg/core": "^0.12.6", + "@ffmpeg/ffmpeg": "^0.12.7", + "@ffmpeg/util": "^0.12.1", + "@mui/icons-material": "^6.4.2", + "@mui/material": "^6.4.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-draggable": "^4.4.6" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.17.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^15.14.0", + "vite": "^6.0.5" + } +} diff --git a/live-wallpaper-creator/public/overlays/README.md b/live-wallpaper-creator/public/overlays/README.md new file mode 100644 index 0000000..ce8557e --- /dev/null +++ b/live-wallpaper-creator/public/overlays/README.md @@ -0,0 +1,36 @@ +# Overlay Assets + +This directory contains overlay assets for the Live Wallpaper Creator. + +## Structure + +``` +overlays/ +├── sparkles/ # Sparkle and glitter effects +├── effects/ # General visual effects +├── nature/ # Nature-themed animations +└── animals/ # Animal animations +``` + +## Adding New Overlays + +1. Add your GIF/APNG file to the appropriate category directory +2. Update overlays.json with the new overlay information: + +```json +{ + "category": [ + { + "name": "Display Name", + "file": "filename.gif", + "description": "Brief description" + } + ] +} +``` + +## Requirements + +- Files should be transparent GIF or APNG format +- Keep file sizes reasonable (< 1MB recommended) +- Use appropriate category or create new one if needed diff --git a/live-wallpaper-creator/public/overlays/overlays.json b/live-wallpaper-creator/public/overlays/overlays.json new file mode 100644 index 0000000..739868c --- /dev/null +++ b/live-wallpaper-creator/public/overlays/overlays.json @@ -0,0 +1,34 @@ +{ + "sparkles": [ + { + "name": "Sparkle Effect", + "file": "sparkle.gif", + "preview": "previews/sparkle.svg", + "description": "Add sparkle effects to your wallpaper" + } + ], + "effects": [ + { + "name": "Glow Effect", + "file": "glow.gif", + "preview": "previews/glow.svg", + "description": "Add a glowing effect" + } + ], + "nature": [ + { + "name": "Falling Leaves", + "file": "leaves.gif", + "preview": "previews/leaf.svg", + "description": "Animated falling leaves effect" + } + ], + "animals": [ + { + "name": "Butterfly", + "file": "butterfly.gif", + "preview": "previews/butterfly.svg", + "description": "Animated butterfly overlay" + } + ] +} diff --git a/live-wallpaper-creator/public/overlays/previews/butterfly.svg b/live-wallpaper-creator/public/overlays/previews/butterfly.svg new file mode 100644 index 0000000..e8a8ce2 --- /dev/null +++ b/live-wallpaper-creator/public/overlays/previews/butterfly.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/live-wallpaper-creator/public/overlays/previews/glow.svg b/live-wallpaper-creator/public/overlays/previews/glow.svg new file mode 100644 index 0000000..0ea48e6 --- /dev/null +++ b/live-wallpaper-creator/public/overlays/previews/glow.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/live-wallpaper-creator/public/overlays/previews/leaf.svg b/live-wallpaper-creator/public/overlays/previews/leaf.svg new file mode 100644 index 0000000..ae7faf3 --- /dev/null +++ b/live-wallpaper-creator/public/overlays/previews/leaf.svg @@ -0,0 +1,12 @@ + + + + + diff --git a/live-wallpaper-creator/public/overlays/previews/sparkle.svg b/live-wallpaper-creator/public/overlays/previews/sparkle.svg new file mode 100644 index 0000000..7c32eed --- /dev/null +++ b/live-wallpaper-creator/public/overlays/previews/sparkle.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/live-wallpaper-creator/public/vite.svg b/live-wallpaper-creator/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/live-wallpaper-creator/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/live-wallpaper-creator/src/App.css b/live-wallpaper-creator/src/App.css new file mode 100644 index 0000000..f376002 --- /dev/null +++ b/live-wallpaper-creator/src/App.css @@ -0,0 +1,53 @@ +#root { + height: 100vh; + width: 100vw; + overflow: hidden; +} + +.canvas-container { + position: relative; + flex-grow: 1; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.2); + border-radius: 8px; + margin: 16px; +} + +.overlay-element { + position: absolute; + cursor: move; + user-select: none; +} + +.overlay-element img { + width: 100%; + height: 100%; + pointer-events: none; +} + +.drag-handle { + position: absolute; + width: 10px; + height: 10px; + background-color: #90caf9; + border-radius: 50%; + cursor: pointer; +} + +.drag-handle.top-left { top: -5px; left: -5px; } +.drag-handle.top-right { top: -5px; right: -5px; } +.drag-handle.bottom-left { bottom: -5px; left: -5px; } +.drag-handle.bottom-right { bottom: -5px; right: -5px; } + +.overlay-controls { + position: absolute; + top: 8px; + right: 8px; + display: flex; + gap: 4px; + background-color: rgba(0, 0, 0, 0.5); + padding: 4px; + border-radius: 4px; +} diff --git a/live-wallpaper-creator/src/App.jsx b/live-wallpaper-creator/src/App.jsx new file mode 100644 index 0000000..b6b04ea --- /dev/null +++ b/live-wallpaper-creator/src/App.jsx @@ -0,0 +1,113 @@ +import { useState, useEffect } from 'react'; +import { ThemeProvider, CssBaseline, Box, AppBar, Toolbar, Typography, Container, IconButton, Tooltip, LinearProgress } from '@mui/material'; +import { createTheme } from '@mui/material/styles'; +import HelpIcon from '@mui/icons-material/Help'; +import Canvas from './components/Canvas'; +import Sidebar from './components/Sidebar'; +import Controls from './components/Controls'; +import BrowserWarning from './components/BrowserWarning'; +import HelpDialog from './components/HelpDialog'; +import WelcomeDialog from './components/WelcomeDialog'; +import ErrorBoundary from './components/ErrorBoundary'; +import { checkBrowserCompatibility } from './utils/browser-check'; + +const theme = createTheme({ + palette: { + mode: 'dark', + primary: { + main: '#90caf9', + }, + secondary: { + main: '#f48fb1', + }, + background: { + default: '#121212', + paper: '#1e1e1e', + }, + }, +}); + +function App() { + const [resolution, setResolution] = useState(null); + const [backgroundImage, setBackgroundImage] = useState(null); + const [overlays, setOverlays] = useState([]); + const [browserIssues, setBrowserIssues] = useState([]); + const [helpOpen, setHelpOpen] = useState(false); + const [welcomeOpen, setWelcomeOpen] = useState(true); + const [ffmpegLoading, setFfmpegLoading] = useState(false); + + useEffect(() => { + const { isCompatible, issues } = checkBrowserCompatibility(); + if (!isCompatible) { + setBrowserIssues(issues); + } + + const handleKeyPress = (e) => { + if (e.key === '?' && !e.ctrlKey && !e.metaKey && !e.altKey) { + setHelpOpen(prev => !prev); + } + }; + + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, []); + + return ( + + + + {browserIssues.length > 0 ? ( + + ) : !resolution ? ( + <> + + setWelcomeOpen(false)} /> + + ) : ( + + + + + Live Wallpaper Creator + + + setHelpOpen(true)} + aria-label="Show help" + > + + + + + {ffmpegLoading && ( + + )} + + + + setOverlays([...overlays, overlay])} + /> + + + + setHelpOpen(false)} /> + + )} + + + ); +} + +export default App; diff --git a/live-wallpaper-creator/src/assets/react.svg b/live-wallpaper-creator/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/live-wallpaper-creator/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/live-wallpaper-creator/src/components/BrowserWarning.jsx b/live-wallpaper-creator/src/components/BrowserWarning.jsx new file mode 100644 index 0000000..c38375b --- /dev/null +++ b/live-wallpaper-creator/src/components/BrowserWarning.jsx @@ -0,0 +1,102 @@ +import { Alert, Box, Typography, List, ListItem, Paper, Divider } from '@mui/material'; +import ErrorIcon from '@mui/icons-material/Error'; +import WarningIcon from '@mui/icons-material/Warning'; + +const BrowserWarning = ({ issues }) => { + const criticalIssues = issues.filter(issue => issue.critical); + const nonCriticalIssues = issues.filter(issue => !issue.critical); + + return ( + + + } + sx={{ + mb: 3, + '& .MuiAlert-message': { width: '100%' } + }} + > + + Browser Compatibility Issues Detected + + + Your browser doesn't meet all the requirements needed to run this application. + Please address the following issues: + + + + {criticalIssues.length > 0 && ( + + + + Critical Issues + + + {criticalIssues.map((issue, index) => ( + + + {issue.feature} + + + {issue.message} + + + Solution: {issue.solution} + + + ))} + + + )} + + {nonCriticalIssues.length > 0 && ( + <> + {criticalIssues.length > 0 && } + + + + Performance Warnings + + + {nonCriticalIssues.map((issue, index) => ( + + + {issue.feature} + + + {issue.message} + + + Solution: {issue.solution} + + + ))} + + + + )} + + + + For optimal performance, please use Chrome, Firefox, or Edge (latest versions) with HTTPS enabled. + + + + + ); +}; + +export default BrowserWarning; diff --git a/live-wallpaper-creator/src/components/Canvas.jsx b/live-wallpaper-creator/src/components/Canvas.jsx new file mode 100644 index 0000000..37d7602 --- /dev/null +++ b/live-wallpaper-creator/src/components/Canvas.jsx @@ -0,0 +1,260 @@ +import { useRef, useEffect, useState, useCallback } from 'react'; +import { Box, Button, ButtonGroup, Tooltip, CircularProgress, Alert, Snackbar, IconButton } from '@mui/material'; +import { styled } from '@mui/material/styles'; +import Draggable from 'react-draggable'; +import { exportAsGif, exportAsMP4 } from '../utils/export'; +import SaveIcon from '@mui/icons-material/Save'; +import MovieIcon from '@mui/icons-material/Movie'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import LinearProgress from '@mui/material/LinearProgress'; + +const Input = styled('input')({ + display: 'none', +}); + +const Canvas = ({ resolution, backgroundImage, overlays, onBackgroundSet, onOverlaysChange }) => { + const canvasRef = useRef(null); + const fileInputRef = useRef(null); + const [scale, setScale] = useState(1); + const [exporting, setExporting] = useState(false); + const [error, setError] = useState(null); + const [progress, setProgress] = useState(0); + + useEffect(() => { + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + const containerWidth = canvas.parentElement.clientWidth; + const containerHeight = canvas.parentElement.clientHeight; + + const scaleX = containerWidth / resolution.width; + const scaleY = containerHeight / resolution.height; + const newScale = Math.min(scaleX, scaleY) * 0.9; + setScale(newScale); + + canvas.style.width = `${resolution.width * newScale}px`; + canvas.style.height = `${resolution.height * newScale}px`; + + canvas.width = resolution.width; + canvas.height = resolution.height; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (backgroundImage) { + ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height); + } + + overlays.forEach(overlay => { + if (overlay.image) { + ctx.drawImage( + overlay.image, + overlay.x, + overlay.y, + overlay.width, + overlay.height + ); + } + }); + }, [resolution, backgroundImage, overlays]); + + const handleKeyPress = useCallback((e) => { + if (e.key === 'o' && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + fileInputRef.current?.click(); + } else if (e.key === 's' && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + if (!exporting && backgroundImage) { + handleExport(e.shiftKey ? 'mp4' : 'gif'); + } + } + }, [exporting, backgroundImage]); + + useEffect(() => { + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, [handleKeyPress]); + + const handleFileInput = (e) => { + const file = e.target.files[0]; + if (file && file.type.startsWith('image/')) { + const reader = new FileReader(); + reader.onload = (event) => { + const img = new Image(); + img.onload = () => onBackgroundSet(img); + img.src = event.target.result; + }; + reader.readAsDataURL(file); + } + }; + + const handleDrop = (e) => { + e.preventDefault(); + const file = e.dataTransfer.files[0]; + if (file && file.type.startsWith('image/')) { + const reader = new FileReader(); + reader.onload = (event) => { + const img = new Image(); + img.onload = () => onBackgroundSet(img); + img.src = event.target.result; + }; + reader.readAsDataURL(file); + } + }; + + const handleExport = async (type) => { + try { + setExporting(true); + setError(null); + setProgress(0); + const canvas = canvasRef.current; + let blob; + + const updateProgress = (p) => { + setProgress(Math.round(p * 100)); + }; + + if (type === 'gif') { + blob = await exportAsGif(canvas, 30, updateProgress); + } else { + blob = await exportAsMP4(canvas, 30, updateProgress); + } + + // Check file size + if (blob.size > 100 * 1024 * 1024) { // 100MB + setError('Warning: The exported file is quite large. You might want to reduce the resolution or duration.'); + } + + // Create download link + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `wallpaper.${type}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (error) { + console.error(`${type.toUpperCase()} export failed:`, error); + setError(`Failed to export ${type.toUpperCase()}: ${error.message}`); + } finally { + setExporting(false); + setProgress(0); + } + }; + + return ( + + + + + + + + + + {exporting && ( + + + + )} + e.preventDefault()} + role="region" + aria-label="Canvas area" + > + {!backgroundImage && ( + + + Drag and drop an image or click to upload (Ctrl/⌘ + O) + + )} + + + setError(null)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + setError(null)} + severity="error" + sx={{ width: '100%' }} + role="alert" + > + {error} + + + + ); +}; + +export default Canvas; diff --git a/live-wallpaper-creator/src/components/Controls.jsx b/live-wallpaper-creator/src/components/Controls.jsx new file mode 100644 index 0000000..e6b1f1e --- /dev/null +++ b/live-wallpaper-creator/src/components/Controls.jsx @@ -0,0 +1,83 @@ +import { useState } from 'react'; +import { + Box, + Container, + Paper, + Typography, + Button, + Select, + MenuItem, + FormControl, + InputLabel, +} from '@mui/material'; + +const COMMON_RESOLUTIONS = [ + { label: '1920x1080 (Full HD)', width: 1920, height: 1080 }, + { label: '2560x1440 (2K)', width: 2560, height: 1440 }, + { label: '3840x2160 (4K)', width: 3840, height: 2160 }, + { label: '1280x720 (HD)', width: 1280, height: 720 }, +]; + +const Controls = ({ onResolutionSet }) => { + const [selectedResolution, setSelectedResolution] = useState(''); + + const handleResolutionSelect = (event) => { + setSelectedResolution(event.target.value); + }; + + const handleContinue = () => { + const resolution = COMMON_RESOLUTIONS.find( + (res) => res.label === selectedResolution + ); + if (resolution) { + onResolutionSet({ width: resolution.width, height: resolution.height }); + } + }; + + return ( + + + + Live Wallpaper Creator + + + Choose your wallpaper resolution to get started + + + Select Resolution + + + + + + ); +}; + +export default Controls; diff --git a/live-wallpaper-creator/src/components/ErrorBoundary.jsx b/live-wallpaper-creator/src/components/ErrorBoundary.jsx new file mode 100644 index 0000000..53edb98 --- /dev/null +++ b/live-wallpaper-creator/src/components/ErrorBoundary.jsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { Box, Typography, Button, Alert, Link } from '@mui/material'; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null }; + } + + static getDerivedStateFromError(error) { + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + this.setState({ + error: error, + errorInfo: errorInfo + }); + console.error('Error caught by boundary:', error, errorInfo); + } + + getErrorMessage() { + const { error } = this.state; + if (!error) return 'An unexpected error occurred'; + + switch (error.name) { + case 'FFmpegError': + return 'Failed to initialize FFmpeg. This could be due to browser compatibility issues.'; + case 'BrowserCompatibilityError': + return `Your browser is missing required features: ${error.features?.join(', ')}`; + case 'ExportError': + return `Failed to export ${error.type.toUpperCase()}: ${error.message}`; + default: + return error.message || 'An unexpected error occurred'; + } + } + + getErrorDetails() { + const { error } = this.state; + if (!error) return null; + + switch (error.name) { + case 'FFmpegError': + return 'Please make sure you\'re using a modern browser with SharedArrayBuffer support and HTTPS enabled.'; + case 'BrowserCompatibilityError': + return 'Please try using Chrome, Firefox, or Edge (latest versions) with HTTPS enabled.'; + case 'ExportError': + return 'Try reducing the resolution or duration of your wallpaper.'; + default: + return null; + } + } + + render() { + if (this.state.hasError) { + return ( + + + + Something went wrong + + + {this.getErrorMessage()} + + {this.getErrorDetails() && ( + + {this.getErrorDetails()} + + )} + {this.state.error?.name === 'BrowserCompatibilityError' && ( + + Learn more about{' '} + + browser requirements + + + )} + + + {process.env.NODE_ENV === 'development' && ( + + + {this.state.errorInfo?.componentStack} + + + )} + + ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/live-wallpaper-creator/src/components/HelpDialog.jsx b/live-wallpaper-creator/src/components/HelpDialog.jsx new file mode 100644 index 0000000..5a4489f --- /dev/null +++ b/live-wallpaper-creator/src/components/HelpDialog.jsx @@ -0,0 +1,99 @@ +import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, List, ListItem, ListItemText, Divider } from '@mui/material'; + +const HelpDialog = ({ open, onClose }) => { + return ( + + Live Wallpaper Creator Help + + + Keyboard Shortcuts + + + + + + + + + + + + + + + + + + + + Features + + + + + + + + + + + + + + + + + Tips + + + + + + + + + + + + + + + ); +}; + +export default HelpDialog; diff --git a/live-wallpaper-creator/src/components/Sidebar.jsx b/live-wallpaper-creator/src/components/Sidebar.jsx new file mode 100644 index 0000000..894c1a4 --- /dev/null +++ b/live-wallpaper-creator/src/components/Sidebar.jsx @@ -0,0 +1,167 @@ +import { useState, useEffect } from 'react'; +import { + Paper, + Box, + Typography, + List, + ListItem, + ListItemText, + ListItemIcon, + Collapse, + IconButton, + CircularProgress, + Alert, +} from '@mui/material'; +import ExpandLess from '@mui/icons-material/ExpandLess'; +import ExpandMore from '@mui/icons-material/ExpandMore'; + +const CATEGORIES = { + sparkles: 'Sparkles', + effects: 'Effects', + nature: 'Nature', + animals: 'Animals', +}; + +const Sidebar = ({ onOverlaySelect }) => { + const [openCategory, setOpenCategory] = useState(null); + const [overlays, setOverlays] = useState({}); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const loadOverlays = async () => { + try { + const response = await fetch('./overlays/overlays.json'); + if (!response.ok) throw new Error('Failed to load overlays'); + const data = await response.json(); + setOverlays(data); + } catch (err) { + console.error('Failed to load overlays:', err); + setError('Failed to load overlays. Please try again later.'); + } finally { + setLoading(false); + } + }; + + loadOverlays(); + }, []); + + const handleCategoryClick = (category) => { + setOpenCategory(openCategory === category ? null : category); + }; + + const handleOverlayClick = async (category, overlay) => { + try { + const response = await fetch(`./overlays/${category}/${overlay.file}`); + if (!response.ok) throw new Error('Failed to load overlay'); + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const img = new Image(); + img.onload = () => { + onOverlaySelect({ + image: img, + x: 0, + y: 0, + width: img.width, + height: img.height, + name: overlay.name, + }); + }; + img.src = url; + } catch (err) { + console.error('Failed to load overlay:', err); + setError('Failed to load overlay. Please try again.'); + } + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + + + Overlay Elements + + {error && ( + + {error} + + )} + + + {Object.entries(CATEGORIES).map(([key, label]) => ( + + handleCategoryClick(key)} + sx={{ bgcolor: openCategory === key ? 'action.selected' : 'inherit' }} + > + + {openCategory === key ? : } + + + + {overlays[key]?.map((overlay, index) => ( + handleOverlayClick(key, overlay)} + > + {overlay.preview && ( + + + + )} + + + )) || ( + + + + )} + + + + ))} + + + ); +}; + +export default Sidebar; diff --git a/live-wallpaper-creator/src/components/WelcomeDialog.jsx b/live-wallpaper-creator/src/components/WelcomeDialog.jsx new file mode 100644 index 0000000..b70b83f --- /dev/null +++ b/live-wallpaper-creator/src/components/WelcomeDialog.jsx @@ -0,0 +1,618 @@ +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Typography, + List, + ListItem, + ListItemIcon, + ListItemText, + Divider, + Box, + Link, + CircularProgress +} from '@mui/material'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import AddIcon from '@mui/icons-material/Add'; +import SaveIcon from '@mui/icons-material/Save'; +import HelpIcon from '@mui/icons-material/Help'; +import WarningIcon from '@mui/icons-material/Warning'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import LaptopIcon from '@mui/icons-material/Laptop'; + +const WelcomeDialog = ({ open, onClose }) => { + const getStartedRef = React.useRef(null); + const [loading, setLoading] = React.useState(true); + + // Focus the "Get Started" button when dialog opens + React.useEffect(() => { + if (open && getStartedRef.current) { + setTimeout(() => { + getStartedRef.current.focus(); + }, 100); + } + }, [open]); + + const [error, setError] = React.useState(null); + const [retryCount, setRetryCount] = React.useState(0); + const maxRetries = 3; + + const [checkProgress, setCheckProgress] = React.useState(0); + const [currentCheck, setCurrentCheck] = React.useState(''); + + const checkFeatures = async () => { + const features = [ + { name: 'SharedArrayBuffer', check: () => typeof SharedArrayBuffer !== 'undefined', label: 'Checking memory support...' }, + { name: 'WebAssembly', check: () => typeof WebAssembly !== 'undefined', label: 'Checking WebAssembly support...' }, + { name: 'Canvas', check: () => typeof HTMLCanvasElement !== 'undefined', label: 'Checking canvas support...' }, + { name: 'Blob', check: () => typeof Blob !== 'undefined', label: 'Checking file support...' }, + { name: 'File API', check: () => typeof File !== 'undefined' && typeof FileReader !== 'undefined', label: 'Checking file API support...' } + ]; + + const missingFeatures = []; + for (let i = 0; i < features.length; i++) { + const feature = features[i]; + setCurrentCheck(feature.label); + setCheckProgress((i + 1) / (features.length + 1)); + await new Promise(resolve => setTimeout(resolve, 200)); + + if (!feature.check()) { + missingFeatures.push(feature.name); + } + } + + if (missingFeatures.length > 0) { + throw new Error(`Missing required features: ${missingFeatures.join(', ')}`); + } + + // Check for secure context + setCurrentCheck('Checking security context...'); + setCheckProgress(1); + await new Promise(resolve => setTimeout(resolve, 200)); + + if (!window.isSecureContext) { + throw new Error('This app requires a secure context (HTTPS) to function properly.'); + } + }; + + const handleRetry = () => { + if (retryCount < maxRetries) { + setRetryCount(prev => prev + 1); + setLoading(true); + setError(null); + } + }; + + // Check browser compatibility + React.useEffect(() => { + if (open) { + const runCheck = async () => { + try { + await checkFeatures(); + setLoading(false); + } catch (err) { + setError(err.message); + setLoading(false); + } + }; + runCheck(); + } + }, [open, retryCount]); + + const handleKeyDown = (event) => { + // Close dialog on Escape + if (event.key === 'Escape') { + onClose(); + } + }; + + const handleGetStarted = () => { + if (!loading) { + onClose(); + } + }; + return ( + { + handleKeyDown(e); + // Handle tab key to keep focus within dialog + if (e.key === 'Tab') { + const focusableElements = e.currentTarget.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + const firstFocusable = focusableElements[0]; + const lastFocusable = focusableElements[focusableElements.length - 1]; + + if (e.shiftKey) { + if (document.activeElement === firstFocusable) { + e.preventDefault(); + lastFocusable.focus(); + } + } else { + if (document.activeElement === lastFocusable) { + e.preventDefault(); + firstFocusable.focus(); + } + } + } + }} + aria-labelledby="welcome-dialog-title" + aria-describedby="welcome-dialog-description" + maxWidth="md" + fullWidth + disableEscapeKeyDown={false} + keepMounted={false} + TransitionProps={{ + onEntered: () => { + if (!loading && !error && getStartedRef.current) { + getStartedRef.current.focus(); + } + } + }} + PaperProps={{ + sx: { + outline: 'none', + '& *:focus-visible': { + outline: '2px solid', + outlineColor: 'primary.main', + outlineOffset: 2, + } + } + }} + // Ensure dialog is modal for proper focus management + disableEnforceFocus={false} + disableAutoFocus={false} + > + + Welcome to Live Wallpaper Creator + + Create stunning animated wallpapers with an accessible and easy-to-use interface + + + + + This dialog provides information about the Live Wallpaper Creator app, including steps to create wallpapers, system requirements, keyboard shortcuts, and accessibility features. + + + Create animated wallpapers in just a few steps: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + System Requirements + + + + + This app uses advanced features for video processing. Please ensure your system meets these requirements: + + + + + + + + + + + + + + + Chrome (v90+), Firefox (v85+), or Edge (v90+) + + Safari has limited support + + + } + /> + + + + + + + + + + + + Learn more about{' '} + + browser compatibility + + + + + + + + Performance Tips + + + + For the best experience: + + + + + + + + + + + + + + + + + + + + + + + + + + + + Keyboard Shortcuts + + + + + + ? + + } + secondary="Show/hide help dialog" + secondaryTypographyProps={{ + 'aria-label': 'Press question mark key to show or hide help dialog' + }} + /> + + + + Delete + + } + secondary="Remove selected overlay" + secondaryTypographyProps={{ + 'aria-label': 'Press delete key to remove the selected overlay' + }} + /> + + + + Space + + } + secondary="Play/pause preview" + secondaryTypographyProps={{ + 'aria-label': 'Press space bar to play or pause the preview' + }} + /> + + + + Ctrl + Z + + } + secondary="Undo last change" + secondaryTypographyProps={{ + 'aria-label': 'Press Control plus Z to undo the last change' + }} + /> + + + + Ctrl + Y + + } + secondary="Redo last change" + secondaryTypographyProps={{ + 'aria-label': 'Press Control plus Y to redo the last change' + }} + /> + + + + + + + + Accessibility Features + + + + + + + + + + + + + + + + + + + + + + + + + + + {loading ? ( + + + {currentCheck || 'Checking browser compatibility...'} + + + + + {Math.round(checkProgress * 100)}% + + + + ) : error ? ( + { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleRetry(); + } + }} + sx={{ + minWidth: 'auto', + p: 0.5, + '&:focus-visible': { + outline: '2px solid', + outlineColor: 'error.main', + outlineOffset: 2, + } + }} + aria-label={`Retry browser compatibility check (${maxRetries - retryCount} attempts remaining)`} + > + Retry ({maxRetries - retryCount}) + + ) : null + } + > + + {error} + + + Please try using a modern browser with HTTPS enabled. + {retryCount >= maxRetries && ( + + Maximum retry attempts reached. Please try again later or use a different browser. + + )} + + + ) : null} + + + + ); +}; + +export default WelcomeDialog; diff --git a/live-wallpaper-creator/src/index.css b/live-wallpaper-creator/src/index.css new file mode 100644 index 0000000..19bc3ad --- /dev/null +++ b/live-wallpaper-creator/src/index.css @@ -0,0 +1,44 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow: hidden; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); +} + +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} + +.MuiPaper-root { + backdrop-filter: blur(8px); + background-color: rgba(30, 30, 30, 0.8) !important; +} + +.MuiAppBar-root { + background-color: rgba(18, 18, 18, 0.95) !important; + backdrop-filter: blur(8px); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} diff --git a/live-wallpaper-creator/src/main.jsx b/live-wallpaper-creator/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/live-wallpaper-creator/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/live-wallpaper-creator/src/utils/browser-check.js b/live-wallpaper-creator/src/utils/browser-check.js new file mode 100644 index 0000000..9b8f619 --- /dev/null +++ b/live-wallpaper-creator/src/utils/browser-check.js @@ -0,0 +1,161 @@ +const getBrowserInfo = () => { + const ua = navigator.userAgent; + const browsers = { + chrome: /chrome|chromium|crios/i.test(ua), + firefox: /firefox|fxios/i.test(ua), + safari: /safari/i.test(ua), + edge: /edg/i.test(ua), + ie: /msie|trident/i.test(ua), + mobile: /mobile|android|iphone|ipad|ipod/i.test(ua), + }; + + // Chrome detection needs to filter out other Chromium-based browsers + if (browsers.chrome) { + browsers.chrome = !browsers.edge; + } + + // Safari detection needs to filter out Chrome and other browsers on iOS + if (browsers.safari) { + browsers.safari = !browsers.chrome && !browsers.firefox && !browsers.edge; + } + + // Get browser version + let version = ''; + if (browsers.chrome) { + version = ua.match(/(?:chrome|chromium|crios)\/(\d+)/i)?.[1] || ''; + } else if (browsers.firefox) { + version = ua.match(/(?:firefox|fxios)\/(\d+)/i)?.[1] || ''; + } else if (browsers.safari) { + version = ua.match(/version\/(\d+)/i)?.[1] || ''; + } else if (browsers.edge) { + version = ua.match(/edg\/(\d+)/i)?.[1] || ''; + } + + return { + ...browsers, + version: parseInt(version, 10) || 0, + isMobile: browsers.mobile + }; +}; + +const MINIMUM_VERSIONS = { + chrome: 90, + firefox: 85, + edge: 90, + safari: 15 +}; + +const getRecommendedBrowser = (currentBrowser) => { + if (currentBrowser.isMobile) { + return 'This app is optimized for desktop browsers. Please use a desktop computer for the best experience.'; + } + if (currentBrowser.ie) { + return 'Internet Explorer is not supported. Please upgrade to a modern browser like Chrome, Firefox, or Edge.'; + } + if (currentBrowser.safari) { + return 'Safari has limited support for some features. Please use Chrome or Firefox for best compatibility.'; + } + + const browserName = Object.keys(MINIMUM_VERSIONS).find(name => currentBrowser[name]) || ''; + if (browserName && currentBrowser.version < MINIMUM_VERSIONS[browserName]) { + return `Your ${browserName} version (${currentBrowser.version}) is outdated. Please update to version ${MINIMUM_VERSIONS[browserName]} or later.`; + } + + return 'Please use the latest version of Chrome, Firefox, or Edge.'; +}; + +export const checkBrowserCompatibility = () => { + const issues = []; + const browser = getBrowserInfo(); + + // Add mobile warning if applicable + if (browser.isMobile) { + issues.push({ + feature: 'Desktop Browser Required', + message: 'This app is optimized for desktop browsers.', + solution: 'Please use a desktop computer for the best experience.', + critical: true + }); + } + + // Check browser version + const browserName = Object.keys(MINIMUM_VERSIONS).find(name => browser[name]); + if (browserName && browser.version < MINIMUM_VERSIONS[browserName]) { + issues.push({ + feature: 'Browser Version', + message: `Your ${browserName} version (${browser.version}) is outdated.`, + solution: `Please update to version ${MINIMUM_VERSIONS[browserName]} or later.`, + critical: true + }); + } + + // Check for SharedArrayBuffer support + if (typeof SharedArrayBuffer === 'undefined') { + issues.push({ + feature: 'SharedArrayBuffer', + message: 'SharedArrayBuffer is not supported in your browser.', + solution: `This feature is required for video processing. ${getRecommendedBrowser(browser)}`, + critical: true + }); + } + + // Check for secure context (HTTPS) + if (!window.isSecureContext) { + issues.push({ + feature: 'Secure Context', + message: 'This app requires a secure context (HTTPS) to function properly.', + solution: 'Please access this app using HTTPS or localhost.', + critical: true + }); + } + + // Check for WebAssembly support + if (typeof WebAssembly === 'undefined') { + issues.push({ + feature: 'WebAssembly', + message: 'WebAssembly is not supported in your browser.', + solution: `This feature is required for video processing. ${getRecommendedBrowser(browser)}`, + critical: true + }); + } + + // Check for Cross-Origin Isolation + if (!crossOriginIsolated) { + issues.push({ + feature: 'Cross-Origin Isolation', + message: 'Cross-Origin Isolation is not enabled.', + solution: 'This site needs to be served with COOP and COEP headers for optimal performance.', + critical: false + }); + } + + // Check for required Web APIs + const requiredAPIs = [ + { name: 'Blob', check: () => typeof Blob !== 'undefined' }, + { name: 'Canvas', check: () => typeof HTMLCanvasElement !== 'undefined' }, + { name: 'File API', check: () => typeof File !== 'undefined' && typeof FileReader !== 'undefined' }, + ]; + + requiredAPIs.forEach(({ name, check }) => { + if (!check()) { + issues.push({ + feature: name, + message: `${name} is not supported in your browser.`, + solution: getRecommendedBrowser(browser), + critical: true + }); + } + }); + + return { + isCompatible: !issues.some(issue => issue.critical), + issues, + browser: { + name: Object.entries(browser) + .filter(([key, value]) => value && key !== 'version' && key !== 'isMobile') + .map(([name]) => name)[0], + version: browser.version, + isMobile: browser.isMobile + } + }; +}; diff --git a/live-wallpaper-creator/src/utils/errors.js b/live-wallpaper-creator/src/utils/errors.js new file mode 100644 index 0000000..a887cda --- /dev/null +++ b/live-wallpaper-creator/src/utils/errors.js @@ -0,0 +1,43 @@ +export class FFmpegError extends Error { + constructor(message, details = {}) { + super(message); + this.name = 'FFmpegError'; + this.details = details; + } +} + +export class BrowserCompatibilityError extends Error { + constructor(message, features = []) { + super(message); + this.name = 'BrowserCompatibilityError'; + this.features = features; + } +} + +export class ExportError extends Error { + constructor(message, type, details = {}) { + super(message); + this.name = 'ExportError'; + this.type = type; + this.details = details; + } +} + +export const createFFmpegError = (error) => { + if (error.message.includes('SharedArrayBuffer')) { + return new BrowserCompatibilityError( + 'SharedArrayBuffer is not available. Please use a modern browser with HTTPS.', + ['SharedArrayBuffer'] + ); + } + if (error.message.includes('WebAssembly')) { + return new BrowserCompatibilityError( + 'WebAssembly is not supported in your browser.', + ['WebAssembly'] + ); + } + return new FFmpegError(error.message, { + originalError: error, + timestamp: new Date().toISOString() + }); +}; diff --git a/live-wallpaper-creator/src/utils/export.js b/live-wallpaper-creator/src/utils/export.js new file mode 100644 index 0000000..07191a2 --- /dev/null +++ b/live-wallpaper-creator/src/utils/export.js @@ -0,0 +1,148 @@ +import { FFmpeg } from '@ffmpeg/ffmpeg'; +import { toBlobURL } from '@ffmpeg/util'; +import { FFmpegError, ExportError, createFFmpegError } from './errors'; + +let ffmpeg = null; + +const initFFmpeg = async () => { + if (!ffmpeg) { + ffmpeg = new FFmpeg(); + try { + const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd'; + const coreURL = await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'); + const wasmURL = await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'); + const workerURL = await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, 'text/javascript'); + + console.log('Loading FFmpeg with URLs:', { coreURL, wasmURL, workerURL }); + + await ffmpeg.load({ + coreURL, + wasmURL, + workerURL, + logger: ({ message }) => console.log('FFmpeg:', message) + }); + + // Test FFmpeg initialization + await ffmpeg.writeFile('test.txt', new Uint8Array([1])); + await ffmpeg.deleteFile('test.txt'); + console.log('FFmpeg initialized successfully'); + } catch (error) { + console.error('FFmpeg initialization failed:', error); + throw createFFmpegError(error); + } + } + return ffmpeg; +}; + +export const exportAsGif = async (canvas, fps = 30, onProgress = () => {}) => { + const frames = []; + const frameCount = (fps * 3); // 3 seconds + + try { + // Capture frames + for (let i = 0; i < frameCount; i++) { + frames.push(canvas.toDataURL('image/png')); + onProgress(i / (frameCount * 2)); // First half of progress for frame capture + await new Promise(resolve => setTimeout(resolve, 1000 / fps)); + } + + const ffmpeg = await initFFmpeg(); + + // Convert frames to GIF + for (let i = 0; i < frames.length; i++) { + const base64Data = frames[i].replace(/^data:image\/\w+;base64,/, ''); + const buffer = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0)); + await ffmpeg.writeFile(`frame_${i}.png`, buffer); + onProgress(0.5 + (i / (frames.length * 2))); // Second half of progress for conversion + } + + await ffmpeg.exec([ + '-framerate', `${fps}`, + '-pattern_type', 'glob', + '-i', 'frame_*.png', + '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2', + 'output.gif' + ]); + + // Read the output file + const data = await ffmpeg.readFile('output.gif'); + + // Clean up files + for (let i = 0; i < frames.length; i++) { + await ffmpeg.deleteFile(`frame_${i}.png`); + } + await ffmpeg.deleteFile('output.gif'); + + onProgress(1); // Complete + return new Blob([data.buffer], { type: 'image/gif' }); + } catch (error) { + console.error('GIF export failed:', error); + throw new ExportError( + error.message, + 'gif', + { + originalError: error, + frameCount, + fps, + timestamp: new Date().toISOString() + } + ); + } +}; + +export const exportAsMP4 = async (canvas, fps = 30, onProgress = () => {}) => { + const frames = []; + const frameCount = (fps * 3); // 3 seconds + + try { + // Capture frames + for (let i = 0; i < frameCount; i++) { + frames.push(canvas.toDataURL('image/png')); + onProgress(i / (frameCount * 2)); // First half of progress for frame capture + await new Promise(resolve => setTimeout(resolve, 1000 / fps)); + } + + const ffmpeg = await initFFmpeg(); + + // Convert frames to video + for (let i = 0; i < frames.length; i++) { + const base64Data = frames[i].replace(/^data:image\/\w+;base64,/, ''); + const buffer = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0)); + await ffmpeg.writeFile(`frame_${i}.png`, buffer); + onProgress(0.5 + (i / (frames.length * 2))); // Second half of progress for conversion + } + + await ffmpeg.exec([ + '-framerate', `${fps}`, + '-pattern_type', 'glob', + '-i', 'frame_*.png', + '-c:v', 'libx264', + '-pix_fmt', 'yuv420p', + 'output.mp4' + ]); + + // Read the output file + const data = await ffmpeg.readFile('output.mp4'); + + // Clean up files + for (let i = 0; i < frames.length; i++) { + await ffmpeg.deleteFile(`frame_${i}.png`); + } + await ffmpeg.deleteFile('output.mp4'); + + onProgress(1); // Complete + return new Blob([data.buffer], { type: 'video/mp4' }); + } catch (error) { + console.error('MP4 export failed:', error); + throw new ExportError( + error.message, + 'mp4', + { + originalError: error, + frameCount, + fps, + timestamp: new Date().toISOString() + } + ); + } +}; diff --git a/live-wallpaper-creator/src/utils/ffmpeg-init.js b/live-wallpaper-creator/src/utils/ffmpeg-init.js new file mode 100644 index 0000000..76b7ef7 --- /dev/null +++ b/live-wallpaper-creator/src/utils/ffmpeg-init.js @@ -0,0 +1,43 @@ +import { FFmpeg } from '@ffmpeg/ffmpeg'; +import { toBlobURL } from '@ffmpeg/util'; + +let ffmpeg = null; + +export const initFFmpeg = async () => { + if (!ffmpeg) { + ffmpeg = new FFmpeg(); + try { + const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd'; + const coreURL = await toBlobURL( + `${baseURL}/ffmpeg-core.js`, + 'text/javascript' + ); + const wasmURL = await toBlobURL( + `${baseURL}/ffmpeg-core.wasm`, + 'application/wasm' + ); + const workerURL = await toBlobURL( + `${baseURL}/ffmpeg-core.worker.js`, + 'text/javascript' + ); + + console.log('Loading FFmpeg with URLs:', { coreURL, wasmURL, workerURL }); + + await ffmpeg.load({ + coreURL, + wasmURL, + workerURL, + logger: ({ message }) => console.log('FFmpeg:', message) + }); + + // Test FFmpeg initialization + await ffmpeg.writeFile('test.txt', new Uint8Array([1])); + await ffmpeg.deleteFile('test.txt'); + console.log('FFmpeg initialized successfully'); + } catch (error) { + console.error('FFmpeg initialization failed:', error); + throw new Error(`Failed to initialize FFmpeg: ${error.message}`); + } + } + return ffmpeg; +}; diff --git a/live-wallpaper-creator/vite.config.js b/live-wallpaper-creator/vite.config.js new file mode 100644 index 0000000..66bf01e --- /dev/null +++ b/live-wallpaper-creator/vite.config.js @@ -0,0 +1,33 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + base: '/hive/pr-161/live-wallpaper-creator/', + server: { + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, + }, + preview: { + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, + }, + optimizeDeps: { + exclude: ['@ffmpeg/ffmpeg', '@ffmpeg/util'] + }, + build: { + target: 'esnext', + rollupOptions: { + output: { + manualChunks: { + ffmpeg: ['@ffmpeg/ffmpeg', '@ffmpeg/util'] + } + } + } + } +})