@teknyo/react-native-splash-generator
Version:
Automatically generate splash screens for React Native projects across iOS, Android, and Web platforms with support for dark mode, branding, and custom configurations.
1 lines • 108 kB
Source Map (JSON)
{"version":3,"sources":["../../src/bin/cli.ts","../../src/platforms/android.ts","../../src/utils/image.ts","../../src/templates/android-templates.ts","../../src/platforms/ios.ts","../../src/templates/ios-templates.ts","../../src/platforms/web.ts","../../src/core/config.ts","../../src/utils/validation.ts","../../src/core/flavor-helper.ts","../../src/controllers/splash-screen.ts","../../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport { generateSplash, removeSplash, generateFlavorSplashes } from '../index.js';\n\nconst program = new Command();\n\nprogram\n .name('react-native-splash')\n .description('React Native Splash Screen Generator')\n .version('1.0.0');\n\nprogram\n .command('generate')\n .description('Generate splash screen assets')\n .option('-c, --config <path>', 'Path to config file')\n .option('-f, --flavor <name>', 'Generate splash screen for specific flavor')\n .action(async options => {\n try {\n await generateSplash(options.config, options.flavor);\n console.log(chalk.green('✓ Splash screen generation completed successfully!'));\n } catch (error) {\n console.error(chalk.red('✗ Error generating splash screen:'), error);\n process.exit(1);\n }\n });\n\nprogram\n .command('remove')\n .description('Remove splash screen assets')\n .option('-f, --flavor <name>', 'Remove splash screen for specific flavor')\n .action(async options => {\n try {\n await removeSplash(options.flavor);\n console.log(chalk.green('✓ Splash screen removed successfully!'));\n } catch (error) {\n console.error(chalk.red('✗ Error removing splash screen:'), error);\n process.exit(1);\n }\n });\n\nprogram\n .command('generate-flavors')\n .description('Generate splash screens for all flavors')\n .action(async () => {\n try {\n await generateFlavorSplashes();\n console.log(chalk.green('✓ All flavor splash screens generated successfully!'));\n } catch (error) {\n console.error(chalk.red('✗ Error generating flavor splash screens:'), error);\n process.exit(1);\n }\n });\n\nprogram.parse(process.argv);\n","import fs from 'fs-extra';\nimport path from 'path';\nimport { parseString, Builder } from 'xml2js';\nimport { ImageProcessor } from '../utils/image';\nimport { SplashConfig } from '../types/config';\nimport { AndroidTemplates } from '../templates/android-templates';\nimport { FlavorHelper } from '../core/flavor-helper';\n\nexport class AndroidPlatform {\n constructor(private flavorHelper: FlavorHelper) {}\n\n async generateSplash(config: SplashConfig): Promise<void> {\n console.log('[Android] Generating splash screen...');\n\n // Generate images\n await this.generateImages(config);\n\n // Generate background files\n await this.generateBackgrounds(config);\n\n // Update XML files\n await this.updateLaunchBackground(config);\n await this.updateStyles(config);\n\n // Update AndroidManifest.xml for orientation\n if (config.androidScreenOrientation) {\n await this.updateManifestOrientation(config.androidScreenOrientation);\n }\n\n console.log('[Android] Splash screen generation complete!');\n }\n\n private async generateImages(config: SplashConfig): Promise<void> {\n const imagePath = config.imageAndroid || config.image;\n const darkImagePath = config.darkImageAndroid || config.darkImage;\n const brandingPath = config.brandingAndroid || config.branding;\n const brandingDarkPath = config.brandingDarkAndroid || config.brandingDark;\n\n // Main splash images\n if (imagePath) {\n await this.generateImageSet(imagePath, 'splash.png', false);\n }\n\n if (darkImagePath) {\n await this.generateImageSet(darkImagePath, 'splash.png', true);\n }\n\n // Branding images\n if (brandingPath) {\n await this.generateImageSet(brandingPath, 'branding.png', false);\n }\n\n if (brandingDarkPath) {\n await this.generateImageSet(brandingDarkPath, 'branding.png', true);\n }\n\n // Android 12 images\n console.log('tjhis is for android 12 splash', config);\n if (config.android12?.image) {\n await this.generateImageSet(config.android12.image, 'android12splash.png', false, true);\n }\n\n if (config.android12?.darkImage) {\n await this.generateImageSet(config.android12.darkImage, 'android12splash.png', true, true);\n }\n\n if (config.android12?.branding) {\n await this.generateImageSet(config.android12.branding, 'android12branding.png', false, true);\n }\n\n if (config.android12?.brandingDark) {\n await this.generateImageSet(\n config.android12.brandingDark,\n 'android12branding.png',\n true,\n true\n );\n }\n }\n\n private async generateImageSet(\n imagePath: string,\n fileName: string,\n dark = false,\n android12 = false\n ): Promise<void> {\n const templates = ImageProcessor.getAndroidTemplates(dark, android12);\n await ImageProcessor.processImage(\n imagePath,\n templates.map(t => ({ ...t, fileName })),\n this.flavorHelper.androidMainResFolder\n );\n }\n\n private async generateBackgrounds(config: SplashConfig): Promise<void> {\n const color = config.colorAndroid || config.color;\n const darkColor = config.darkColorAndroid || config.darkColor;\n const backgroundImage = config.backgroundImageAndroid || config.backgroundImage;\n const darkBackgroundImage = config.darkBackgroundImageAndroid || config.darkBackgroundImage;\n\n // Light background\n await this.createBackground(\n color,\n backgroundImage,\n path.join(this.flavorHelper.androidDrawableFolder, 'background.png')\n );\n\n // Dark background\n if (darkColor || darkBackgroundImage) {\n await this.createBackground(\n darkColor,\n darkBackgroundImage,\n path.join(this.flavorHelper.androidDrawableNightFolder, 'background.png')\n );\n }\n\n // Also create v21 versions\n await this.createBackground(\n color,\n backgroundImage,\n path.join(this.flavorHelper.androidMainResFolder, 'drawable-v21/background.png')\n );\n\n if (darkColor || darkBackgroundImage) {\n await this.createBackground(\n darkColor,\n darkBackgroundImage,\n path.join(this.flavorHelper.androidMainResFolder, 'drawable-night-v21/background.png')\n );\n }\n }\n\n private async createBackground(\n color?: string,\n backgroundImage?: string,\n outputPath?: string\n ): Promise<void> {\n if (!outputPath) return;\n\n if (backgroundImage) {\n await ImageProcessor.copyAndConvertImage(backgroundImage, outputPath, 'png');\n } else if (color) {\n await ImageProcessor.createSolidColorImage(color, 1, 1, outputPath);\n }\n }\n\n private async updateLaunchBackground(config: SplashConfig): Promise<void> {\n const gravity = config.androidGravity || 'center';\n const showImage = !!(config.imageAndroid || config.image);\n const showBranding = !!(config.brandingAndroid || config.branding);\n const brandingMode = config.brandingMode || 'bottom';\n const brandingPadding =\n config.brandingBottomPaddingAndroid || config.brandingBottomPadding || 0;\n\n // Update main launch background\n await this.writeLaunchBackgroundXml(\n this.flavorHelper.androidLaunchBackgroundFile,\n gravity,\n showImage,\n showBranding,\n brandingMode,\n brandingPadding\n );\n\n // Update dark launch background if needed\n const hasDarkBackground = !!(\n config.darkColorAndroid ||\n config.darkColor ||\n config.darkBackgroundImageAndroid ||\n config.darkBackgroundImage\n );\n\n if (hasDarkBackground) {\n await this.writeLaunchBackgroundXml(\n this.flavorHelper.androidLaunchDarkBackgroundFile,\n gravity,\n showImage,\n showBranding,\n brandingMode,\n brandingPadding\n );\n }\n }\n\n private async writeLaunchBackgroundXml(\n filePath: string,\n gravity: string,\n showImage: boolean,\n showBranding: boolean,\n brandingMode: string,\n brandingPadding: number\n ): Promise<void> {\n console.log(`[Android] Updating ${filePath}`);\n\n await fs.ensureDir(path.dirname(filePath));\n\n let xml = AndroidTemplates.launchBackgroundXml;\n\n if (showImage) {\n xml = xml.replace(\n '</layer-list>',\n ` <item>\n <bitmap android:gravity=\"${gravity}\" android:src=\"@drawable/splash\" />\n </item>\n</layer-list>`\n );\n }\n\n if (showBranding && brandingMode !== gravity) {\n let brandingGravity = brandingMode;\n if (brandingMode === 'bottomRight') {\n brandingGravity = 'bottom|right';\n } else if (brandingMode === 'bottomLeft') {\n brandingGravity = 'bottom|left';\n }\n\n const brandingItem = AndroidTemplates.brandingItemXml\n .replace('{bottom_padding}', brandingPadding.toString())\n .replace('center', brandingGravity);\n\n xml = xml.replace('</layer-list>', ` ${brandingItem}</layer-list>`);\n }\n\n await fs.writeFile(filePath, xml);\n }\n\n private async updateStyles(config: SplashConfig): Promise<void> {\n console.log('[Android] Updating styles...');\n\n const fullscreen = config.fullscreen || false;\n\n // Update main styles\n await this.updateStylesFile(\n this.flavorHelper.androidStylesFile,\n AndroidTemplates.stylesXml,\n fullscreen\n );\n\n // Update night styles\n await this.updateStylesFile(\n this.flavorHelper.androidStylesNightFile,\n AndroidTemplates.stylesNightXml,\n fullscreen\n );\n\n // Update v31 styles (Android 12+)\n await this.updateStylesFile(\n this.flavorHelper.androidStylesV31File,\n AndroidTemplates.stylesV31Xml,\n fullscreen,\n config.android12\n );\n\n // Update v31 night styles\n await this.updateStylesFile(\n this.flavorHelper.androidStylesV31NightFile,\n AndroidTemplates.stylesV31NightXml,\n fullscreen,\n config.android12\n );\n }\n\n private async updateStylesFile(\n filePath: string,\n template: string,\n fullscreen: boolean,\n android12Config?: any\n ): Promise<void> {\n console.log(`[Android] Updating ${filePath}`);\n\n await fs.ensureDir(path.dirname(filePath));\n\n if (!(await fs.pathExists(filePath))) {\n await fs.writeFile(filePath, template);\n }\n\n const content = await fs.readFile(filePath, 'utf-8');\n\n parseString(content, async (err, result) => {\n if (err) {\n console.error(`Error parsing ${filePath}:`, err);\n return;\n }\n\n // Find LaunchTheme style\n const resources = result.resources;\n if (!resources || !resources.style) return;\n\n const launchTheme = resources.style.find((style: any) => style.$.name === 'LaunchTheme');\n\n if (!launchTheme) return;\n\n // Update items\n if (!launchTheme.item) launchTheme.item = [];\n\n this.updateStyleItem(launchTheme, 'android:forceDarkAllowed', 'false');\n this.updateStyleItem(launchTheme, 'android:windowFullscreen', fullscreen.toString());\n this.updateStyleItem(\n launchTheme,\n 'android:windowDrawsSystemBarBackgrounds',\n (!fullscreen).toString()\n );\n this.updateStyleItem(launchTheme, 'android:windowLayoutInDisplayCutoutMode', 'shortEdges');\n\n // Android 12 specific styles\n if (android12Config && filePath.includes('v31')) {\n if (android12Config.color) {\n this.updateStyleItem(\n launchTheme,\n 'android:windowSplashScreenBackground',\n `#${android12Config.color}`\n );\n }\n if (android12Config.image) {\n this.updateStyleItem(\n launchTheme,\n 'android:windowSplashScreenAnimatedIcon',\n '@drawable/android12splash'\n );\n }\n if (android12Config.iconBackgroundColor) {\n this.updateStyleItem(\n launchTheme,\n 'android:windowSplashScreenIconBackgroundColor',\n `#${android12Config.iconBackgroundColor}`\n );\n }\n if (android12Config.branding) {\n this.updateStyleItem(\n launchTheme,\n 'android:windowSplashScreenBrandingImage',\n '@drawable/android12branding'\n );\n }\n }\n\n // Convert back to XML\n const builder = new Builder({ headless: true });\n const xml = builder.buildObject(result);\n await fs.writeFile(filePath, `<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n${xml}\\n`);\n });\n }\n\n private updateStyleItem(launchTheme: any, name: string, value: string): void {\n const existingIndex = launchTheme.item.findIndex((item: any) => item.$.name === name);\n\n if (existingIndex >= 0) {\n launchTheme.item[existingIndex]._ = value;\n } else {\n launchTheme.item.push({\n $: { name },\n _: value,\n });\n }\n }\n\n private async updateManifestOrientation(orientation: string): Promise<void> {\n const manifestPath = this.flavorHelper.androidManifestFile;\n\n if (!(await fs.pathExists(manifestPath))) {\n console.warn('[Android] AndroidManifest.xml not found, skipping orientation update');\n return;\n }\n\n console.log('[Android] Updating AndroidManifest.xml orientation...');\n\n const content = await fs.readFile(manifestPath, 'utf-8');\n\n parseString(content, async (err, result) => {\n if (err) {\n console.error('Error parsing AndroidManifest.xml:', err);\n return;\n }\n\n const manifest = result.manifest;\n const application = manifest?.application?.[0];\n const activity = application?.activity?.[0];\n\n if (activity) {\n activity.$['android:screenOrientation'] = orientation;\n\n const builder = new Builder({ headless: true });\n const xml = builder.buildObject(result);\n await fs.writeFile(manifestPath, `<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n${xml}\\n`);\n }\n });\n }\n\n async removeSplash(): Promise<void> {\n console.log('[Android] Removing splash screen...');\n\n // Remove image files\n const imageFolders = [\n 'drawable-mdpi',\n 'drawable-hdpi',\n 'drawable-xhdpi',\n 'drawable-xxhdpi',\n 'drawable-xxxhdpi',\n 'drawable-night-mdpi',\n 'drawable-night-hdpi',\n 'drawable-night-xhdpi',\n 'drawable-night-xxhdpi',\n 'drawable-night-xxxhdpi',\n ];\n\n for (const folder of imageFolders) {\n const folderPath = path.join(this.flavorHelper.androidMainResFolder, folder);\n if (await fs.pathExists(folderPath)) {\n const splashFile = path.join(folderPath, 'splash.png');\n const brandingFile = path.join(folderPath, 'branding.png');\n\n if (await fs.pathExists(splashFile)) {\n await fs.remove(splashFile);\n }\n if (await fs.pathExists(brandingFile)) {\n await fs.remove(brandingFile);\n }\n }\n }\n\n console.log('[Android] Splash screen removal complete!');\n }\n}\n","import sharp from 'sharp';\nimport fs from 'fs-extra';\nimport path from 'path';\n\nexport interface ImageTemplate {\n fileName: string;\n pixelDensity: number;\n directory?: string;\n}\n\nexport interface ImageResizeOptions {\n width?: number;\n height?: number;\n fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside';\n background?: string;\n}\n\nexport class ImageProcessor {\n static async processImage(\n sourcePath: string,\n templates: ImageTemplate[],\n baseOutputPath: string,\n options: ImageResizeOptions = {}\n ): Promise<void> {\n const sourceImage = sharp(sourcePath);\n const { width: originalWidth, height: originalHeight } = await sourceImage.metadata();\n\n if (!originalWidth || !originalHeight) {\n throw new Error(`Unable to get dimensions for image: ${sourcePath}`);\n }\n\n await Promise.all(\n templates.map(async template => {\n const outputDir = template.directory\n ? path.join(baseOutputPath, template.directory)\n : baseOutputPath;\n\n const outputPath = path.join(outputDir, template.fileName);\n\n // Ensure output directory exists\n await fs.ensureDir(outputDir);\n\n // Calculate target dimensions\n const targetWidth = Math.round((originalWidth * template.pixelDensity) / 4);\n const targetHeight = Math.round((originalHeight * template.pixelDensity) / 4);\n\n // Resize and save image\n await sourceImage\n .resize({\n width: options.width || targetWidth,\n height: options.height || targetHeight,\n fit: options.fit || 'inside',\n background: options.background || { r: 255, g: 255, b: 255, alpha: 0 },\n })\n .png()\n .toFile(outputPath);\n })\n );\n }\n\n static async createSolidColorImage(\n color: string,\n width: number,\n height: number,\n outputPath: string\n ): Promise<void> {\n await fs.ensureDir(path.dirname(outputPath));\n\n const { r, g, b } = this.hexToRgb(color);\n\n await sharp({\n create: {\n width,\n height,\n channels: 3,\n background: { r, g, b },\n },\n })\n .png()\n .toFile(outputPath);\n }\n\n static async copyAndConvertImage(\n sourcePath: string,\n outputPath: string,\n targetFormat: 'png' | 'jpg' | 'gif' = 'png'\n ): Promise<void> {\n await fs.ensureDir(path.dirname(outputPath));\n\n const sourceExt = path.extname(sourcePath).toLowerCase();\n const targetExt = path.extname(outputPath).toLowerCase();\n\n // If same format, just copy\n if (sourceExt === `.${targetFormat}` && sourceExt === targetExt) {\n await fs.copy(sourcePath, outputPath);\n return;\n }\n\n // Convert using Sharp\n let processor = sharp(sourcePath);\n\n switch (targetFormat) {\n case 'png':\n processor = processor.png();\n break;\n case 'jpg':\n processor = processor.jpeg();\n break;\n case 'gif':\n // Sharp doesn't support GIF output, copy if source is GIF\n if (sourceExt === '.gif') {\n await fs.copy(sourcePath, outputPath);\n return;\n }\n processor = processor.png(); // Fallback to PNG\n break;\n }\n\n await processor.toFile(outputPath);\n }\n\n static hexToRgb(hex: string): { r: number; g: number; b: number } {\n const cleanHex = hex.replace('#', '');\n const r = parseInt(cleanHex.substr(0, 2), 16);\n const g = parseInt(cleanHex.substr(2, 2), 16);\n const b = parseInt(cleanHex.substr(4, 2), 16);\n return { r, g, b };\n }\n\n static async getImageDimensions(imagePath: string): Promise<{ width: number; height: number }> {\n const metadata = await sharp(imagePath).metadata();\n if (!metadata.width || !metadata.height) {\n throw new Error(`Unable to get dimensions for image: ${imagePath}`);\n }\n return { width: metadata.width, height: metadata.height };\n }\n\n // Android density templates\n static getAndroidTemplates(dark = false, android12 = false): ImageTemplate[] {\n const prefix = dark ? 'drawable-night' : 'drawable';\n const suffix = android12 ? '-v31' : '';\n const templates: ImageTemplate[] = [];\n\n // Regular splash screen templates\n const densities = [\n { name: 'mdpi', scale: 1 },\n { name: 'hdpi', scale: 1.5 },\n { name: 'xhdpi', scale: 2 },\n { name: 'xxhdpi', scale: 3 },\n { name: 'xxxhdpi', scale: 4 },\n ];\n\n // Add regular splash.png for each density\n densities.forEach(density => {\n templates.push({\n fileName: 'splash.png',\n pixelDensity: density.scale,\n directory: `${prefix}-${density.name}${suffix}`,\n });\n\n // If Android 12 support is enabled, add android12splash.png to drawable folder\n if (android12) {\n // Add to regular drawable\n templates.push({\n fileName: 'android12splash.png',\n pixelDensity: density.scale, // Use highest quality for Android 12\n directory: `${prefix}-${density.name}${suffix}`,\n });\n\n // Add to night drawable if dark mode\n if (dark) {\n templates.push({\n fileName: 'android12splash.png',\n pixelDensity: density.scale, // Use highest quality for Android 12\n directory: 'drawable-night',\n });\n }\n\n // Optional: Add branding image if needed\n templates.push({\n fileName: 'android12branding.png',\n pixelDensity: 4,\n directory: 'drawable',\n });\n }\n });\n\n return templates;\n }\n\n // iOS density templates\n static getIOSTemplates(imageName = 'LaunchImage', dark = false): ImageTemplate[] {\n const suffix = dark ? 'Dark' : '';\n return [\n { fileName: `${imageName}${suffix}.png`, pixelDensity: 1 },\n { fileName: `${imageName}${suffix}@2x.png`, pixelDensity: 2 },\n { fileName: `${imageName}${suffix}@3x.png`, pixelDensity: 3 },\n ];\n }\n\n // Web density templates\n static getWebTemplates(prefix = 'light', extension = 'png'): ImageTemplate[] {\n return [\n { fileName: `${prefix}-1x.${extension}`, pixelDensity: 1 },\n { fileName: `${prefix}-2x.${extension}`, pixelDensity: 2 },\n { fileName: `${prefix}-3x.${extension}`, pixelDensity: 3 },\n { fileName: `${prefix}-4x.${extension}`, pixelDensity: 4 },\n ];\n }\n}\n","export const AndroidTemplates = {\n launchBackgroundXml: `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <item>\n <bitmap android:src=\"@drawable/background\" android:gravity=\"fill\" />\n </item>\n</layer-list>`,\n\n brandingItemXml: `<item>\n <bitmap android:gravity=\"{bottom_padding}\" android:src=\"@drawable/branding\" />\n </item>`,\n\n stylesXml: `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <style name=\"LaunchTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n <item name=\"android:windowBackground\">@drawable/launch_background</item>\n <item name=\"android:forceDarkAllowed\">false</item>\n <item name=\"android:windowFullscreen\">false</item>\n <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</item>\n </style>\n</resources>`,\n\n stylesNightXml: `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <style name=\"LaunchTheme\" parent=\"Theme.AppCompat.DayNight.NoActionBar\">\n <item name=\"android:windowBackground\">@drawable/launch_background</item>\n <item name=\"android:forceDarkAllowed\">false</item>\n <item name=\"android:windowFullscreen\">false</item>\n <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</item>\n </style>\n</resources>`,\n\n stylesV31Xml: `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <style name=\"LaunchTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n <item name=\"android:windowSplashScreenBackground\">#ffffff</item>\n <item name=\"android:windowSplashScreenAnimatedIcon\">@drawable/android12splash</item>\n <item name=\"android:windowSplashScreenIconBackgroundColor\">#ffffff</item>\n <item name=\"android:windowSplashScreenBrandingImage\">@drawable/android12branding</item>\n <item name=\"android:forceDarkAllowed\">false</item>\n <item name=\"android:windowFullscreen\">false</item>\n <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</item>\n </style>\n</resources>`,\n\n stylesV31NightXml: `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <style name=\"LaunchTheme\" parent=\"Theme.AppCompat.DayNight.NoActionBar\">\n <item name=\"android:windowSplashScreenBackground\">#000000</item>\n <item name=\"android:windowSplashScreenAnimatedIcon\">@drawable/android12splash</item>\n <item name=\"android:windowSplashScreenIconBackgroundColor\">#000000</item>\n <item name=\"android:windowSplashScreenBrandingImage\">@drawable/android12branding</item>\n <item name=\"android:forceDarkAllowed\">false</item>\n <item name=\"android:windowFullscreen\">false</item>\n <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</item>\n </style>\n</resources>`,\n};\n","import fs from 'fs-extra';\nimport path from 'path';\nimport plist from 'plist';\nimport { parseString, Builder } from 'xml2js';\nimport { ImageProcessor } from '../utils/image';\nimport { SplashConfig } from '../types/config';\nimport { IOSTemplates } from '../templates/ios-templates';\nimport { FlavorHelper } from '../core/flavor-helper';\n\nexport class IOSPlatform {\n constructor(private flavorHelper: FlavorHelper) {}\n\n async generateSplash(config: SplashConfig): Promise<void> {\n console.log('[iOS] Generating splash screen...');\n\n // Generate images\n await this.generateImages(config);\n\n // Generate background images\n await this.generateBackgrounds(config);\n\n // Create/update storyboard\n await this.updateLaunchScreenStoryboard(config);\n\n // Update Info.plist files\n await this.updateInfoPlistFiles(config);\n\n console.log('[iOS] Splash screen generation complete!');\n }\n\n private async generateImages(config: SplashConfig): Promise<void> {\n const imagePath = config.imageIos || config.image;\n const darkImagePath = config.darkImageIos || config.darkImage;\n const brandingPath = config.brandingIos || config.branding;\n const brandingDarkPath = config.brandingDarkIos || config.brandingDark;\n\n // Main splash images\n if (imagePath) {\n await this.generateImageSet(\n imagePath,\n this.flavorHelper.iOSAssetsLaunchImageFolder,\n 'LaunchImage',\n false\n );\n } else {\n // Create 1x1 transparent images\n await this.createTransparentImages(\n this.flavorHelper.iOSAssetsLaunchImageFolder,\n 'LaunchImage'\n );\n }\n\n // Dark splash images\n if (darkImagePath) {\n await this.generateImageSet(\n darkImagePath,\n this.flavorHelper.iOSAssetsLaunchImageFolder,\n 'LaunchImage',\n true\n );\n }\n\n // Branding images\n if (brandingPath) {\n await this.generateImageSet(\n brandingPath,\n this.flavorHelper.iOSAssetsBrandingImageFolder,\n 'BrandingImage',\n false\n );\n }\n\n if (brandingDarkPath) {\n await this.generateImageSet(\n brandingDarkPath,\n this.flavorHelper.iOSAssetsBrandingImageFolder,\n 'BrandingImage',\n true\n );\n }\n\n // Create Contents.json files\n await this.createContentsJson(config);\n }\n\n private async generateImageSet(\n imagePath: string,\n outputFolder: string,\n baseName: string,\n dark = false\n ): Promise<void> {\n const templates = ImageProcessor.getIOSTemplates(baseName, dark);\n\n await ImageProcessor.processImage(imagePath, templates, outputFolder);\n }\n\n private async createTransparentImages(outputFolder: string, baseName: string): Promise<void> {\n await fs.ensureDir(outputFolder);\n\n const templates = ImageProcessor.getIOSTemplates(baseName, false);\n\n for (const template of templates) {\n await ImageProcessor.createSolidColorImage(\n 'ffffff',\n 1,\n 1,\n path.join(outputFolder, template.fileName)\n );\n }\n }\n\n private async createContentsJson(config: SplashConfig): Promise<void> {\n const launchImageFolder = this.flavorHelper.iOSAssetsLaunchImageFolder;\n await fs.ensureDir(launchImageFolder);\n\n const hasDarkImage = !!(config.darkImageIos || config.darkImage);\n const contentsJson = hasDarkImage ? IOSTemplates.contentsJsonDark : IOSTemplates.contentsJson;\n\n await fs.writeFile(path.join(launchImageFolder, 'Contents.json'), contentsJson);\n\n // Branding Contents.json\n const brandingPath = config.brandingIos || config.branding;\n if (brandingPath) {\n const brandingFolder = this.flavorHelper.iOSAssetsBrandingImageFolder;\n await fs.ensureDir(brandingFolder);\n\n const hasDarkBranding = !!(config.brandingDarkIos || config.brandingDark);\n const brandingContentsJson = hasDarkBranding\n ? IOSTemplates.brandingContentsJsonDark\n : IOSTemplates.brandingContentsJson;\n\n await fs.writeFile(path.join(brandingFolder, 'Contents.json'), brandingContentsJson);\n }\n }\n\n private async generateBackgrounds(config: SplashConfig): Promise<void> {\n const color = config.colorIos || config.color;\n const darkColor = config.darkColorIos || config.darkColor;\n const backgroundImage = config.backgroundImageIos || config.backgroundImage;\n const darkBackgroundImage = config.darkBackgroundImageIos || config.darkBackgroundImage;\n\n const backgroundFolder = this.flavorHelper.iOSAssetsLaunchBackgroundFolder;\n await fs.ensureDir(backgroundFolder);\n\n // Light background\n if (backgroundImage) {\n await ImageProcessor.copyAndConvertImage(\n backgroundImage,\n path.join(backgroundFolder, 'background.png'),\n 'png'\n );\n } else if (color) {\n await ImageProcessor.createSolidColorImage(\n color,\n 1,\n 1,\n path.join(backgroundFolder, 'background.png')\n );\n }\n\n // Dark background\n if (darkBackgroundImage) {\n await ImageProcessor.copyAndConvertImage(\n darkBackgroundImage,\n path.join(backgroundFolder, 'darkbackground.png'),\n 'png'\n );\n } else if (darkColor) {\n await ImageProcessor.createSolidColorImage(\n darkColor,\n 1,\n 1,\n path.join(backgroundFolder, 'darkbackground.png')\n );\n }\n\n // Create background Contents.json\n const hasDarkBackground = !!(darkColor || darkBackgroundImage);\n const backgroundContentsJson = hasDarkBackground\n ? IOSTemplates.launchBackgroundDarkJson\n : IOSTemplates.launchBackgroundJson;\n\n await fs.writeFile(path.join(backgroundFolder, 'Contents.json'), backgroundContentsJson);\n }\n\n private async updateLaunchScreenStoryboard(config: SplashConfig): Promise<void> {\n const storyboardPath = this.flavorHelper.iOSLaunchScreenStoryboardFile;\n console.log(`[iOS] Updating ${storyboardPath}`);\n\n await fs.ensureDir(path.dirname(storyboardPath));\n\n const imagePath = config.imageIos || config.image;\n const brandingPath = config.brandingIos || config.branding;\n const contentMode = config.iosContentMode || 'center';\n const brandingContentMode = config.iosBrandingContentMode || 'bottom';\n const brandingPadding = config.brandingBottomPaddingIos || config.brandingBottomPadding || 0;\n\n // Create or update storyboard\n if (!(await fs.pathExists(storyboardPath))) {\n const storyboardContent = IOSTemplates.launchScreenStoryboard.replace(\n /\\[LAUNCH_IMAGE_PLACEHOLDER\\]/g,\n this.flavorHelper.iOSLaunchImageName\n );\n\n await fs.writeFile(storyboardPath, storyboardContent);\n }\n\n // Update existing storyboard\n await this.updateStoryboardContent(\n storyboardPath,\n imagePath,\n brandingPath,\n contentMode,\n brandingContentMode,\n brandingPadding\n );\n }\n\n private async updateStoryboardContent(\n storyboardPath: string,\n imagePath?: string,\n brandingPath?: string,\n contentMode = 'center',\n brandingContentMode = 'bottom',\n brandingPadding = 0\n ): Promise<void> {\n const content = await fs.readFile(storyboardPath, 'utf-8');\n\n parseString(content, async (err, result) => {\n if (err) {\n console.error('Error parsing storyboard:', err);\n return;\n }\n\n // Find the main view\n const document = result.document;\n const scenes = document.scenes[0].scene;\n const viewController = scenes[0].objects[0].viewController[0];\n const view = viewController.view[0];\n\n // Update content mode for main image\n if (view.subviews && view.subviews[0].imageView) {\n const imageViews = view.subviews[0].imageView;\n const launchImageView = imageViews.find(\n (iv: any) => iv.$.image === this.flavorHelper.iOSLaunchImageName\n );\n\n if (launchImageView) {\n launchImageView.$.contentMode = contentMode;\n }\n\n // Handle branding image\n if (brandingPath && brandingContentMode !== contentMode) {\n const brandingImageView = imageViews.find(\n (iv: any) => iv.$.image === this.flavorHelper.iOSBrandingImageName\n );\n\n if (!brandingImageView) {\n // Add branding image view\n const brandingSubview = IOSTemplates.brandingSubview.replace(\n '[BRANDING_IMAGE_PLACEHOLDER]',\n this.flavorHelper.iOSBrandingImageName\n );\n\n // Parse and add to subviews\n parseString(`<root>${brandingSubview}</root>`, (err, brandingResult) => {\n if (!err && brandingResult.root.imageView) {\n imageViews.push(brandingResult.root.imageView[0]);\n }\n });\n }\n\n if (brandingImageView) {\n brandingImageView.$.contentMode = brandingContentMode;\n }\n }\n }\n\n // Update constraints for branding\n if (brandingPath && view.constraints) {\n let constraintsXml = IOSTemplates.brandingCenterBottomConstraints;\n\n if (brandingContentMode === 'bottomLeft') {\n constraintsXml = IOSTemplates.brandingLeftBottomConstraints;\n } else if (brandingContentMode === 'bottomRight') {\n constraintsXml = IOSTemplates.brandingRightBottomConstraints;\n }\n\n constraintsXml = constraintsXml.replace('{bottom_padding}', brandingPadding.toString());\n\n // Parse and add constraints\n parseString(`<root>${constraintsXml}</root>`, (err, constraintsResult) => {\n if (!err && constraintsResult.root.constraints) {\n if (!view.constraints[0].constraint) {\n view.constraints[0].constraint = [];\n }\n view.constraints[0].constraint.push(\n ...constraintsResult.root.constraints[0].constraint\n );\n }\n });\n }\n\n // Add background constraints\n if (!view.constraints || !view.constraints[0].constraint) {\n parseString(`<root>${IOSTemplates.launchBackgroundConstraints}</root>`, (err, bgResult) => {\n if (!err && bgResult.root.constraints) {\n view.constraints = bgResult.root.constraints;\n }\n });\n }\n\n // Update image dimensions in resources\n if (imagePath && document.resources && document.resources[0].image) {\n const { width, height } = await ImageProcessor.getImageDimensions(imagePath);\n const launchImageResource = document.resources[0].image.find(\n (img: any) => img.$.name === this.flavorHelper.iOSLaunchImageName\n );\n\n if (launchImageResource) {\n launchImageResource.$.width = width.toString();\n launchImageResource.$.height = height.toString();\n }\n }\n\n // Convert back to XML\n const builder = new Builder({\n headless: true,\n renderOpts: { pretty: true, indent: ' ' },\n });\n const xml = builder.buildObject(result);\n await fs.writeFile(\n storyboardPath,\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\\n${xml}\\n`\n );\n });\n }\n\n private async updateInfoPlistFiles(config: SplashConfig): Promise<void> {\n const plistFiles = config.infoPlistFiles || [this.flavorHelper.iOSInfoPlistFile];\n const fullscreen = config.fullscreen || false;\n\n for (const plistFile of plistFiles) {\n if (!(await fs.pathExists(plistFile))) {\n console.warn(`[iOS] Info.plist file not found: ${plistFile}`);\n continue;\n }\n\n console.log(`[iOS] Updating ${plistFile}`);\n await this.updateInfoPlist(plistFile, fullscreen);\n }\n }\n\n private async updateInfoPlist(plistFile: string, fullscreen: boolean): Promise<void> {\n const content = await fs.readFile(plistFile, 'utf-8');\n const plistData = plist.parse(content) as any;\n\n // Update status bar hidden\n plistData.UIStatusBarHidden = fullscreen;\n\n if (fullscreen) {\n plistData.UIViewControllerBasedStatusBarAppearance = false;\n }\n\n // Convert back to plist format\n const updatedContent = plist.build(plistData);\n await fs.writeFile(plistFile, updatedContent);\n }\n\n async removeSplash(): Promise<void> {\n console.log('[iOS] Removing splash screen...');\n\n // Remove asset folders\n const foldersToRemove = [\n this.flavorHelper.iOSAssetsLaunchImageFolder,\n this.flavorHelper.iOSAssetsBrandingImageFolder,\n this.flavorHelper.iOSAssetsLaunchBackgroundFolder,\n ];\n\n for (const folder of foldersToRemove) {\n if (await fs.pathExists(folder)) {\n await fs.remove(folder);\n }\n }\n\n console.log('[iOS] Splash screen removal complete!');\n }\n}\n","export const IOSTemplates = {\n contentsJson: `{\n \"images\": [\n {\n \"idiom\": \"universal\",\n \"scale\": \"1x\",\n \"filename\": \"LaunchImage.png\"\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"2x\",\n \"filename\": \"LaunchImage@2x.png\"\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"3x\",\n \"filename\": \"LaunchImage@3x.png\"\n }\n ],\n \"info\": {\n \"version\": 1,\n \"author\": \"xcode\"\n }\n}`,\n\n contentsJsonDark: `{\n \"images\": [\n {\n \"idiom\": \"universal\",\n \"scale\": \"1x\",\n \"filename\": \"LaunchImage.png\"\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"2x\",\n \"filename\": \"LaunchImage@2x.png\"\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"3x\",\n \"filename\": \"LaunchImage@3x.png\"\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"1x\",\n \"filename\": \"LaunchImageDark.png\",\n \"appearances\": [\n {\n \"appearance\": \"luminosity\",\n \"value\": \"dark\"\n }\n ]\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"2x\",\n \"filename\": \"LaunchImageDark@2x.png\",\n \"appearances\": [\n {\n \"appearance\": \"luminosity\",\n \"value\": \"dark\"\n }\n ]\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"3x\",\n \"filename\": \"LaunchImageDark@3x.png\",\n \"appearances\": [\n {\n \"appearance\": \"luminosity\",\n \"value\": \"dark\"\n }\n ]\n }\n ],\n \"info\": {\n \"version\": 1,\n \"author\": \"xcode\"\n }\n}`,\n\n brandingContentsJson: `{\n \"images\": [\n {\n \"idiom\": \"universal\",\n \"scale\": \"1x\",\n \"filename\": \"BrandingImage.png\"\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"2x\",\n \"filename\": \"BrandingImage@2x.png\"\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"3x\",\n \"filename\": \"BrandingImage@3x.png\"\n }\n ],\n \"info\": {\n \"version\": 1,\n \"author\": \"xcode\"\n }\n}`,\n\n brandingContentsJsonDark: `{\n \"images\": [\n {\n \"idiom\": \"universal\",\n \"scale\": \"1x\",\n \"filename\": \"BrandingImage.png\"\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"2x\",\n \"filename\": \"BrandingImage@2x.png\"\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"3x\",\n \"filename\": \"BrandingImage@3x.png\"\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"1x\",\n \"filename\": \"BrandingImageDark.png\",\n \"appearances\": [\n {\n \"appearance\": \"luminosity\",\n \"value\": \"dark\"\n }\n ]\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"2x\",\n \"filename\": \"BrandingImageDark@2x.png\",\n \"appearances\": [\n {\n \"appearance\": \"luminosity\",\n \"value\": \"dark\"\n }\n ]\n },\n {\n \"idiom\": \"universal\",\n \"scale\": \"3x\",\n \"filename\": \"BrandingImageDark@3x.png\",\n \"appearances\": [\n {\n \"appearance\": \"luminosity\",\n \"value\": \"dark\"\n }\n ]\n }\n ],\n \"info\": {\n \"version\": 1,\n \"author\": \"xcode\"\n }\n}`,\n\n launchBackgroundJson: `{\n \"images\": [\n {\n \"idiom\": \"universal\",\n \"filename\": \"background.png\"\n }\n ],\n \"info\": {\n \"version\": 1,\n \"author\": \"xcode\"\n }\n}`,\n\n launchBackgroundDarkJson: `{\n \"images\": [\n {\n \"idiom\": \"universal\",\n \"filename\": \"background.png\"\n },\n {\n \"idiom\": \"universal\",\n \"filename\": \"darkbackground.png\",\n \"appearances\": [\n {\n \"appearance\": \"luminosity\",\n \"value\": \"dark\"\n }\n ]\n }\n ],\n \"info\": {\n \"version\": 1,\n \"author\": \"xcode\"\n }\n}`,\n\n launchScreenStoryboard: `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"17156\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" useTraitCollections=\"YES\" useSafeAreas=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n <device id=\"retina6_1\" orientation=\"portrait\" appearance=\"light\"/>\n <dependencies>\n <deployment identifier=\"iOS\"/>\n <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"17126\"/>\n <capability name=\"Safe area layout guides\" minToolsVersion=\"9.0\"/>\n <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n </dependencies>\n <scenes>\n <!--View Controller-->\n <scene sceneID=\"EHf-IW-A2E\">\n <objects>\n <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"896\"/>\n <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n <subviews>\n <imageView clipsSubviews=\"YES\" userInteractionEnabled=\"NO\" contentMode=\"scaleAspectFit\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" image=\"[LAUNCH_IMAGE_PLACEHOLDER]\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k0-Ey4\">\n <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"896\"/>\n </imageView>\n </subviews>\n <viewLayoutGuide key=\"safeArea\" id=\"Bcu-3y-fUS\"/>\n <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n </view>\n </viewController>\n <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n </objects>\n <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n </scene>\n </scenes>\n <resources>\n <image name=\"[LAUNCH_IMAGE_PLACEHOLDER]\" width=\"0.5\" height=\"0.5\"/>\n </resources>\n</document>`,\n\n brandingSubview: `<imageView clipsSubviews=\"YES\" userInteractionEnabled=\"NO\" contentMode=\"scaleAspectFit\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" image=\"[BRANDING_IMAGE_PLACEHOLDER]\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k1-Ey4\">\n <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"896\"/>\n</imageView>`,\n\n launchBackgroundConstraints: `<constraints>\n <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerX\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerX\" id=\"1Gr-gV-wFs\"/>\n <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerY\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerY\" id=\"4X2-HB-R7a\"/>\n <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"leading\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"leading\" id=\"SdS-ul-q2q\"/>\n <constraint firstAttribute=\"trailing\" secondItem=\"YRO-k0-Ey4\" secondAttribute=\"trailing\" id=\"Swv-Gf-Rwn\"/>\n <constraint firstAttribute=\"bottom\" secondItem=\"YRO-k0-Ey4\" secondAttribute=\"bottom\" id=\"Y44-ml-fuU\"/>\n <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"top\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"top\" id=\"moa-c2-u7t\"/>\n</constraints>`,\n\n brandingCenterBottomConstraints: `<constraints>\n <constraint firstItem=\"Bcu-3y-fUS\" firstAttribute=\"bottom\" secondItem=\"YRO-k1-Ey4\" secondAttribute=\"bottom\" constant=\"{bottom_padding}\" id=\"7fp-q4-alv\"/>\n <constraint firstItem=\"YRO-k1-Ey4\" firstAttribute=\"centerX\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerX\" id=\"qky-c2-u7t\"/>\n</constraints>`,\n\n brandingLeftBottomConstraints: `<constraints>\n <constraint firstItem=\"Bcu-3y-fUS\" firstAttribute=\"bottom\" secondItem=\"YRO-k1-Ey4\" secondAttribute=\"bottom\" constant=\"{bottom_padding}\" id=\"7fp-q4-alv\"/>\n <constraint firstItem=\"YRO-k1-Ey4\" firstAttribute=\"leading\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"leading\" id=\"qky-c2-u7t\"/>\n</constraints>`,\n\n brandingRightBottomConstraints: `<constraints>\n <constraint firstItem=\"Bcu-3y-fUS\" firstAttribute=\"bottom\" secondItem=\"YRO-k1-Ey4\" secondAttribute=\"bottom\" constant=\"{bottom_padding}\" id=\"7fp-q4-alv\"/>\n <constraint firstAttribute=\"trailing\" secondItem=\"YRO-k1-Ey4\" secondAttribute=\"trailing\" id=\"qky-c2-u7t\"/>\n</constraints>`,\n};\n","import fs from 'fs-extra';\nimport path from 'path';\nimport { ImageProcessor } from '../utils/image';\nimport { SplashConfig } from '../types/config';\nimport { FlavorHelper } from '../core/flavor-helper';\n\nexport class WebPlatform {\n constructor(private flavorHelper: FlavorHelper) {}\n\n async generateSplash(config: SplashConfig): Promise<void> {\n console.log('[Web] Generating splash screen...');\n\n // Generate images\n await this.generateImages(config);\n\n // Generate background images or colors\n await this.generateBackgrounds(config);\n\n // Update index.html\n await this.updateIndexHtml(config);\n\n console.log('[Web] Splash screen generation complete!');\n }\n\n private async generateImages(config: SplashConfig): Promise<void> {\n const imagePath = config.imageWeb || config.image;\n const darkImagePath = config.darkImageWeb || config.darkImage;\n const brandingPath = config.brandingWeb || config.branding;\n const brandingDarkPath = config.brandingDarkWeb || config.brandingDark;\n\n // Create splash images folder\n await fs.ensureDir(this.flavorHelper.webSplashImagesFolder);\n\n // Main splash images\n if (imagePath) {\n await this.generateImageSet(imagePath, 'light');\n }\n\n if (darkImagePath) {\n await this.generateImageSet(darkImagePath, 'dark');\n }\n\n // Branding images\n if (brandingPath) {\n await this.generateImageSet(brandingPath, 'branding-light');\n }\n\n if (brandingDarkPath) {\n await this.generateImageSet(brandingDarkPath, 'branding-dark');\n }\n }\n\n private async generateImageSet(imagePath: string, prefix: string): Promise<void> {\n const templates = ImageProcessor.getWebTemplates(prefix);\n await ImageProcessor.processImage(\n imagePath,\n templates,\n this.flavorHelper.webSplashImagesFolder\n );\n }\n\n private async generateBackgrounds(config: SplashConfig): Promise<void> {\n const color = config.colorWeb || config.color;\n const darkColor = config.darkColorWeb || config.darkColor;\n const backgroundImage = config.backgroundImageWeb || config.backgroundImage;\n const darkBackgroundImage = config.darkBackgroundImageWeb || config.darkBackgroundImage;\n\n // Create splash background folder\n const backgroundFolder = path.join(this.flavorHelper.webSplashImagesFolder, 'background');\n await fs.ensureDir(backgroundFolder);\n\n // Light background\n if (backgroundImage) {\n await ImageProcessor.copyAndConvertImage(\n backgroundImage,\n path.join(backgroundFolder, 'light.png'),\n 'png'\n );\n } else if (color) {\n await ImageProcessor.createSolidColorImage(\n color,\n 1,\n 1,\n path.join(backgroundFolder, 'light.png')\n );\n }\n\n // Dark background\n if (darkBackgroundImage) {\n await ImageProcessor.copy