UNPKG

@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,386 lines (1,363 loc) 61.7 kB
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/platforms/android.ts import fs2 from "fs-extra"; import path2 from "path"; import { parseString, Builder } from "xml2js"; // src/utils/image.ts import sharp from "sharp"; import fs from "fs-extra"; import path from "path"; var ImageProcessor = class { static async processImage(sourcePath, templates, baseOutputPath, options = {}) { const sourceImage = sharp(sourcePath); const { width: originalWidth, height: originalHeight } = await sourceImage.metadata(); if (!originalWidth || !originalHeight) { throw new Error(`Unable to get dimensions for image: ${sourcePath}`); } await Promise.all( templates.map(async (template) => { const outputDir = template.directory ? path.join(baseOutputPath, template.directory) : baseOutputPath; const outputPath = path.join(outputDir, template.fileName); await fs.ensureDir(outputDir); const targetWidth = Math.round(originalWidth * template.pixelDensity / 4); const targetHeight = Math.round(originalHeight * template.pixelDensity / 4); await sourceImage.resize({ width: options.width || targetWidth, height: options.height || targetHeight, fit: options.fit || "inside", background: options.background || { r: 255, g: 255, b: 255, alpha: 0 } }).png().toFile(outputPath); }) ); } static async createSolidColorImage(color, width, height, outputPath) { await fs.ensureDir(path.dirname(outputPath)); const { r, g, b } = this.hexToRgb(color); await sharp({ create: { width, height, channels: 3, background: { r, g, b } } }).png().toFile(outputPath); } static async copyAndConvertImage(sourcePath, outputPath, targetFormat = "png") { await fs.ensureDir(path.dirname(outputPath)); const sourceExt = path.extname(sourcePath).toLowerCase(); const targetExt = path.extname(outputPath).toLowerCase(); if (sourceExt === `.${targetFormat}` && sourceExt === targetExt) { await fs.copy(sourcePath, outputPath); return; } let processor = sharp(sourcePath); switch (targetFormat) { case "png": processor = processor.png(); break; case "jpg": processor = processor.jpeg(); break; case "gif": if (sourceExt === ".gif") { await fs.copy(sourcePath, outputPath); return; } processor = processor.png(); break; } await processor.toFile(outputPath); } static hexToRgb(hex) { const cleanHex = hex.replace("#", ""); const r = parseInt(cleanHex.substr(0, 2), 16); const g = parseInt(cleanHex.substr(2, 2), 16); const b = parseInt(cleanHex.substr(4, 2), 16); return { r, g, b }; } static async getImageDimensions(imagePath) { const metadata = await sharp(imagePath).metadata(); if (!metadata.width || !metadata.height) { throw new Error(`Unable to get dimensions for image: ${imagePath}`); } return { width: metadata.width, height: metadata.height }; } // Android density templates static getAndroidTemplates(dark = false, android12 = false) { const prefix = dark ? "drawable-night" : "drawable"; const suffix = android12 ? "-v31" : ""; const templates = []; const densities = [ { name: "mdpi", scale: 1 }, { name: "hdpi", scale: 1.5 }, { name: "xhdpi", scale: 2 }, { name: "xxhdpi", scale: 3 }, { name: "xxxhdpi", scale: 4 } ]; densities.forEach((density) => { templates.push({ fileName: "splash.png", pixelDensity: density.scale, directory: `${prefix}-${density.name}${suffix}` }); if (android12) { templates.push({ fileName: "android12splash.png", pixelDensity: density.scale, // Use highest quality for Android 12 directory: `${prefix}-${density.name}${suffix}` }); if (dark) { templates.push({ fileName: "android12splash.png", pixelDensity: density.scale, // Use highest quality for Android 12 directory: "drawable-night" }); } templates.push({ fileName: "android12branding.png", pixelDensity: 4, directory: "drawable" }); } }); return templates; } // iOS density templates static getIOSTemplates(imageName = "LaunchImage", dark = false) { const suffix = dark ? "Dark" : ""; return [ { fileName: `${imageName}${suffix}.png`, pixelDensity: 1 }, { fileName: `${imageName}${suffix}@2x.png`, pixelDensity: 2 }, { fileName: `${imageName}${suffix}@3x.png`, pixelDensity: 3 } ]; } // Web density templates static getWebTemplates(prefix = "light", extension = "png") { return [ { fileName: `${prefix}-1x.${extension}`, pixelDensity: 1 }, { fileName: `${prefix}-2x.${extension}`, pixelDensity: 2 }, { fileName: `${prefix}-3x.${extension}`, pixelDensity: 3 }, { fileName: `${prefix}-4x.${extension}`, pixelDensity: 4 } ]; } }; // src/templates/android-templates.ts var AndroidTemplates = { launchBackgroundXml: `<?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item> <bitmap android:src="@drawable/background" android:gravity="fill" /> </item> </layer-list>`, brandingItemXml: `<item> <bitmap android:gravity="{bottom_padding}" android:src="@drawable/branding" /> </item>`, stylesXml: `<?xml version="1.0" encoding="utf-8"?> <resources> <style name="LaunchTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="android:windowBackground">@drawable/launch_background</item> <item name="android:forceDarkAllowed">false</item> <item name="android:windowFullscreen">false</item> <item name="android:windowDrawsSystemBarBackgrounds">true</item> <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> </style> </resources>`, stylesNightXml: `<?xml version="1.0" encoding="utf-8"?> <resources> <style name="LaunchTheme" parent="Theme.AppCompat.DayNight.NoActionBar"> <item name="android:windowBackground">@drawable/launch_background</item> <item name="android:forceDarkAllowed">false</item> <item name="android:windowFullscreen">false</item> <item name="android:windowDrawsSystemBarBackgrounds">true</item> <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> </style> </resources>`, stylesV31Xml: `<?xml version="1.0" encoding="utf-8"?> <resources> <style name="LaunchTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="android:windowSplashScreenBackground">#ffffff</item> <item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item> <item name="android:windowSplashScreenIconBackgroundColor">#ffffff</item> <item name="android:windowSplashScreenBrandingImage">@drawable/android12branding</item> <item name="android:forceDarkAllowed">false</item> <item name="android:windowFullscreen">false</item> <item name="android:windowDrawsSystemBarBackgrounds">true</item> <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> </style> </resources>`, stylesV31NightXml: `<?xml version="1.0" encoding="utf-8"?> <resources> <style name="LaunchTheme" parent="Theme.AppCompat.DayNight.NoActionBar"> <item name="android:windowSplashScreenBackground">#000000</item> <item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item> <item name="android:windowSplashScreenIconBackgroundColor">#000000</item> <item name="android:windowSplashScreenBrandingImage">@drawable/android12branding</item> <item name="android:forceDarkAllowed">false</item> <item name="android:windowFullscreen">false</item> <item name="android:windowDrawsSystemBarBackgrounds">true</item> <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> </style> </resources>` }; // src/platforms/android.ts var AndroidPlatform = class { constructor(flavorHelper) { this.flavorHelper = flavorHelper; } async generateSplash(config) { console.log("[Android] Generating splash screen..."); await this.generateImages(config); await this.generateBackgrounds(config); await this.updateLaunchBackground(config); await this.updateStyles(config); if (config.androidScreenOrientation) { await this.updateManifestOrientation(config.androidScreenOrientation); } console.log("[Android] Splash screen generation complete!"); } async generateImages(config) { const imagePath = config.imageAndroid || config.image; const darkImagePath = config.darkImageAndroid || config.darkImage; const brandingPath = config.brandingAndroid || config.branding; const brandingDarkPath = config.brandingDarkAndroid || config.brandingDark; if (imagePath) { await this.generateImageSet(imagePath, "splash.png", false); } if (darkImagePath) { await this.generateImageSet(darkImagePath, "splash.png", true); } if (brandingPath) { await this.generateImageSet(brandingPath, "branding.png", false); } if (brandingDarkPath) { await this.generateImageSet(brandingDarkPath, "branding.png", true); } console.log("tjhis is for android 12 splash", config); if (config.android12?.image) { await this.generateImageSet(config.android12.image, "android12splash.png", false, true); } if (config.android12?.darkImage) { await this.generateImageSet(config.android12.darkImage, "android12splash.png", true, true); } if (config.android12?.branding) { await this.generateImageSet(config.android12.branding, "android12branding.png", false, true); } if (config.android12?.brandingDark) { await this.generateImageSet( config.android12.brandingDark, "android12branding.png", true, true ); } } async generateImageSet(imagePath, fileName, dark = false, android12 = false) { const templates = ImageProcessor.getAndroidTemplates(dark, android12); await ImageProcessor.processImage( imagePath, templates.map((t) => ({ ...t, fileName })), this.flavorHelper.androidMainResFolder ); } async generateBackgrounds(config) { const color = config.colorAndroid || config.color; const darkColor = config.darkColorAndroid || config.darkColor; const backgroundImage = config.backgroundImageAndroid || config.backgroundImage; const darkBackgroundImage = config.darkBackgroundImageAndroid || config.darkBackgroundImage; await this.createBackground( color, backgroundImage, path2.join(this.flavorHelper.androidDrawableFolder, "background.png") ); if (darkColor || darkBackgroundImage) { await this.createBackground( darkColor, darkBackgroundImage, path2.join(this.flavorHelper.androidDrawableNightFolder, "background.png") ); } await this.createBackground( color, backgroundImage, path2.join(this.flavorHelper.androidMainResFolder, "drawable-v21/background.png") ); if (darkColor || darkBackgroundImage) { await this.createBackground( darkColor, darkBackgroundImage, path2.join(this.flavorHelper.androidMainResFolder, "drawable-night-v21/background.png") ); } } async createBackground(color, backgroundImage, outputPath) { if (!outputPath) return; if (backgroundImage) { await ImageProcessor.copyAndConvertImage(backgroundImage, outputPath, "png"); } else if (color) { await ImageProcessor.createSolidColorImage(color, 1, 1, outputPath); } } async updateLaunchBackground(config) { const gravity = config.androidGravity || "center"; const showImage = !!(config.imageAndroid || config.image); const showBranding = !!(config.brandingAndroid || config.branding); const brandingMode = config.brandingMode || "bottom"; const brandingPadding = config.brandingBottomPaddingAndroid || config.brandingBottomPadding || 0; await this.writeLaunchBackgroundXml( this.flavorHelper.androidLaunchBackgroundFile, gravity, showImage, showBranding, brandingMode, brandingPadding ); const hasDarkBackground = !!(config.darkColorAndroid || config.darkColor || config.darkBackgroundImageAndroid || config.darkBackgroundImage); if (hasDarkBackground) { await this.writeLaunchBackgroundXml( this.flavorHelper.androidLaunchDarkBackgroundFile, gravity, showImage, showBranding, brandingMode, brandingPadding ); } } async writeLaunchBackgroundXml(filePath, gravity, showImage, showBranding, brandingMode, brandingPadding) { console.log(`[Android] Updating ${filePath}`); await fs2.ensureDir(path2.dirname(filePath)); let xml = AndroidTemplates.launchBackgroundXml; if (showImage) { xml = xml.replace( "</layer-list>", ` <item> <bitmap android:gravity="${gravity}" android:src="@drawable/splash" /> </item> </layer-list>` ); } if (showBranding && brandingMode !== gravity) { let brandingGravity = brandingMode; if (brandingMode === "bottomRight") { brandingGravity = "bottom|right"; } else if (brandingMode === "bottomLeft") { brandingGravity = "bottom|left"; } const brandingItem = AndroidTemplates.brandingItemXml.replace("{bottom_padding}", brandingPadding.toString()).replace("center", brandingGravity); xml = xml.replace("</layer-list>", ` ${brandingItem}</layer-list>`); } await fs2.writeFile(filePath, xml); } async updateStyles(config) { console.log("[Android] Updating styles..."); const fullscreen = config.fullscreen || false; await this.updateStylesFile( this.flavorHelper.androidStylesFile, AndroidTemplates.stylesXml, fullscreen ); await this.updateStylesFile( this.flavorHelper.androidStylesNightFile, AndroidTemplates.stylesNightXml, fullscreen ); await this.updateStylesFile( this.flavorHelper.androidStylesV31File, AndroidTemplates.stylesV31Xml, fullscreen, config.android12 ); await this.updateStylesFile( this.flavorHelper.androidStylesV31NightFile, AndroidTemplates.stylesV31NightXml, fullscreen, config.android12 ); } async updateStylesFile(filePath, template, fullscreen, android12Config) { console.log(`[Android] Updating ${filePath}`); await fs2.ensureDir(path2.dirname(filePath)); if (!await fs2.pathExists(filePath)) { await fs2.writeFile(filePath, template); } const content = await fs2.readFile(filePath, "utf-8"); parseString(content, async (err, result) => { if (err) { console.error(`Error parsing ${filePath}:`, err); return; } const resources = result.resources; if (!resources || !resources.style) return; const launchTheme = resources.style.find((style) => style.$.name === "LaunchTheme"); if (!launchTheme) return; if (!launchTheme.item) launchTheme.item = []; this.updateStyleItem(launchTheme, "android:forceDarkAllowed", "false"); this.updateStyleItem(launchTheme, "android:windowFullscreen", fullscreen.toString()); this.updateStyleItem( launchTheme, "android:windowDrawsSystemBarBackgrounds", (!fullscreen).toString() ); this.updateStyleItem(launchTheme, "android:windowLayoutInDisplayCutoutMode", "shortEdges"); if (android12Config && filePath.includes("v31")) { if (android12Config.color) { this.updateStyleItem( launchTheme, "android:windowSplashScreenBackground", `#${android12Config.color}` ); } if (android12Config.image) { this.updateStyleItem( launchTheme, "android:windowSplashScreenAnimatedIcon", "@drawable/android12splash" ); } if (android12Config.iconBackgroundColor) { this.updateStyleItem( launchTheme, "android:windowSplashScreenIconBackgroundColor", `#${android12Config.iconBackgroundColor}` ); } if (android12Config.branding) { this.updateStyleItem( launchTheme, "android:windowSplashScreenBrandingImage", "@drawable/android12branding" ); } } const builder = new Builder({ headless: true }); const xml = builder.buildObject(result); await fs2.writeFile(filePath, `<?xml version="1.0" encoding="utf-8"?> ${xml} `); }); } updateStyleItem(launchTheme, name, value) { const existingIndex = launchTheme.item.findIndex((item) => item.$.name === name); if (existingIndex >= 0) { launchTheme.item[existingIndex]._ = value; } else { launchTheme.item.push({ $: { name }, _: value }); } } async updateManifestOrientation(orientation) { const manifestPath = this.flavorHelper.androidManifestFile; if (!await fs2.pathExists(manifestPath)) { console.warn("[Android] AndroidManifest.xml not found, skipping orientation update"); return; } console.log("[Android] Updating AndroidManifest.xml orientation..."); const content = await fs2.readFile(manifestPath, "utf-8"); parseString(content, async (err, result) => { if (err) { console.error("Error parsing AndroidManifest.xml:", err); return; } const manifest = result.manifest; const application = manifest?.application?.[0]; const activity = application?.activity?.[0]; if (activity) { activity.$["android:screenOrientation"] = orientation; const builder = new Builder({ headless: true }); const xml = builder.buildObject(result); await fs2.writeFile(manifestPath, `<?xml version="1.0" encoding="utf-8"?> ${xml} `); } }); } async removeSplash() { console.log("[Android] Removing splash screen..."); const imageFolders = [ "drawable-mdpi", "drawable-hdpi", "drawable-xhdpi", "drawable-xxhdpi", "drawable-xxxhdpi", "drawable-night-mdpi", "drawable-night-hdpi", "drawable-night-xhdpi", "drawable-night-xxhdpi", "drawable-night-xxxhdpi" ]; for (const folder of imageFolders) { const folderPath = path2.join(this.flavorHelper.androidMainResFolder, folder); if (await fs2.pathExists(folderPath)) { const splashFile = path2.join(folderPath, "splash.png"); const brandingFile = path2.join(folderPath, "branding.png"); if (await fs2.pathExists(splashFile)) { await fs2.remove(splashFile); } if (await fs2.pathExists(brandingFile)) { await fs2.remove(brandingFile); } } } console.log("[Android] Splash screen removal complete!"); } }; // src/platforms/ios.ts import fs3 from "fs-extra"; import path3 from "path"; import plist from "plist"; import { parseString as parseString2, Builder as Builder2 } from "xml2js"; // src/templates/ios-templates.ts var IOSTemplates = { contentsJson: `{ "images": [ { "idiom": "universal", "scale": "1x", "filename": "LaunchImage.png" }, { "idiom": "universal", "scale": "2x", "filename": "LaunchImage@2x.png" }, { "idiom": "universal", "scale": "3x", "filename": "LaunchImage@3x.png" } ], "info": { "version": 1, "author": "xcode" } }`, contentsJsonDark: `{ "images": [ { "idiom": "universal", "scale": "1x", "filename": "LaunchImage.png" }, { "idiom": "universal", "scale": "2x", "filename": "LaunchImage@2x.png" }, { "idiom": "universal", "scale": "3x", "filename": "LaunchImage@3x.png" }, { "idiom": "universal", "scale": "1x", "filename": "LaunchImageDark.png", "appearances": [ { "appearance": "luminosity", "value": "dark" } ] }, { "idiom": "universal", "scale": "2x", "filename": "LaunchImageDark@2x.png", "appearances": [ { "appearance": "luminosity", "value": "dark" } ] }, { "idiom": "universal", "scale": "3x", "filename": "LaunchImageDark@3x.png", "appearances": [ { "appearance": "luminosity", "value": "dark" } ] } ], "info": { "version": 1, "author": "xcode" } }`, brandingContentsJson: `{ "images": [ { "idiom": "universal", "scale": "1x", "filename": "BrandingImage.png" }, { "idiom": "universal", "scale": "2x", "filename": "BrandingImage@2x.png" }, { "idiom": "universal", "scale": "3x", "filename": "BrandingImage@3x.png" } ], "info": { "version": 1, "author": "xcode" } }`, brandingContentsJsonDark: `{ "images": [ { "idiom": "universal", "scale": "1x", "filename": "BrandingImage.png" }, { "idiom": "universal", "scale": "2x", "filename": "BrandingImage@2x.png" }, { "idiom": "universal", "scale": "3x", "filename": "BrandingImage@3x.png" }, { "idiom": "universal", "scale": "1x", "filename": "BrandingImageDark.png", "appearances": [ { "appearance": "luminosity", "value": "dark" } ] }, { "idiom": "universal", "scale": "2x", "filename": "BrandingImageDark@2x.png", "appearances": [ { "appearance": "luminosity", "value": "dark" } ] }, { "idiom": "universal", "scale": "3x", "filename": "BrandingImageDark@3x.png", "appearances": [ { "appearance": "luminosity", "value": "dark" } ] } ], "info": { "version": 1, "author": "xcode" } }`, launchBackgroundJson: `{ "images": [ { "idiom": "universal", "filename": "background.png" } ], "info": { "version": 1, "author": "xcode" } }`, launchBackgroundDarkJson: `{ "images": [ { "idiom": "universal", "filename": "background.png" }, { "idiom": "universal", "filename": "darkbackground.png", "appearances": [ { "appearance": "luminosity", "value": "dark" } ] } ], "info": { "version": 1, "author": "xcode" } }`, launchScreenStoryboard: `<?xml version="1.0" encoding="UTF-8"?> <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"> <device id="retina6_1" orientation="portrait" appearance="light"/> <dependencies> <deployment identifier="iOS"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17126"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> <scenes> <!--View Controller--> <scene sceneID="EHf-IW-A2E"> <objects> <viewController id="01J-lp-oVM" sceneMemberID="viewController"> <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3"> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="[LAUNCH_IMAGE_PLACEHOLDER]" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> </imageView> </subviews> <viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> </view> </viewController> <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/> </objects> <point key="canvasLocation" x="53" y="375"/> </scene> </scenes> <resources> <image name="[LAUNCH_IMAGE_PLACEHOLDER]" width="0.5" height="0.5"/> </resources> </document>`, brandingSubview: `<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="[BRANDING_IMAGE_PLACEHOLDER]" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k1-Ey4"> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> </imageView>`, launchBackgroundConstraints: `<constraints> <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1Gr-gV-wFs"/> <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/> <constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="SdS-ul-q2q"/> <constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="Swv-Gf-Rwn"/> <constraint firstAttribute="bottom" secondItem="YRO-k0-Ey4" secondAttribute="bottom" id="Y44-ml-fuU"/> <constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="moa-c2-u7t"/> </constraints>`, brandingCenterBottomConstraints: `<constraints> <constraint firstItem="Bcu-3y-fUS" firstAttribute="bottom" secondItem="YRO-k1-Ey4" secondAttribute="bottom" constant="{bottom_padding}" id="7fp-q4-alv"/> <constraint firstItem="YRO-k1-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="qky-c2-u7t"/> </constraints>`, brandingLeftBottomConstraints: `<constraints> <constraint firstItem="Bcu-3y-fUS" firstAttribute="bottom" secondItem="YRO-k1-Ey4" secondAttribute="bottom" constant="{bottom_padding}" id="7fp-q4-alv"/> <constraint firstItem="YRO-k1-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="qky-c2-u7t"/> </constraints>`, brandingRightBottomConstraints: `<constraints> <constraint firstItem="Bcu-3y-fUS" firstAttribute="bottom" secondItem="YRO-k1-Ey4" secondAttribute="bottom" constant="{bottom_padding}" id="7fp-q4-alv"/> <constraint firstAttribute="trailing" secondItem="YRO-k1-Ey4" secondAttribute="trailing" id="qky-c2-u7t"/> </constraints>` }; // src/platforms/ios.ts var IOSPlatform = class { constructor(flavorHelper) { this.flavorHelper = flavorHelper; } async generateSplash(config) { console.log("[iOS] Generating splash screen..."); await this.generateImages(config); await this.generateBackgrounds(config); await this.updateLaunchScreenStoryboard(config); await this.updateInfoPlistFiles(config); console.log("[iOS] Splash screen generation complete!"); } async generateImages(config) { const imagePath = config.imageIos || config.image; const darkImagePath = config.darkImageIos || config.darkImage; const brandingPath = config.brandingIos || config.branding; const brandingDarkPath = config.brandingDarkIos || config.brandingDark; if (imagePath) { await this.generateImageSet( imagePath, this.flavorHelper.iOSAssetsLaunchImageFolder, "LaunchImage", false ); } else { await this.createTransparentImages( this.flavorHelper.iOSAssetsLaunchImageFolder, "LaunchImage" ); } if (darkImagePath) { await this.generateImageSet( darkImagePath, this.flavorHelper.iOSAssetsLaunchImageFolder, "LaunchImage", true ); } if (brandingPath) { await this.generateImageSet( brandingPath, this.flavorHelper.iOSAssetsBrandingImageFolder, "BrandingImage", false ); } if (brandingDarkPath) { await this.generateImageSet( brandingDarkPath, this.flavorHelper.iOSAssetsBrandingImageFolder, "BrandingImage", true ); } await this.createContentsJson(config); } async generateImageSet(imagePath, outputFolder, baseName, dark = false) { const templates = ImageProcessor.getIOSTemplates(baseName, dark); await ImageProcessor.processImage(imagePath, templates, outputFolder); } async createTransparentImages(outputFolder, baseName) { await fs3.ensureDir(outputFolder); const templates = ImageProcessor.getIOSTemplates(baseName, false); for (const template of templates) { await ImageProcessor.createSolidColorImage( "ffffff", 1, 1, path3.join(outputFolder, template.fileName) ); } } async createContentsJson(config) { const launchImageFolder = this.flavorHelper.iOSAssetsLaunchImageFolder; await fs3.ensureDir(launchImageFolder); const hasDarkImage = !!(config.darkImageIos || config.darkImage); const contentsJson = hasDarkImage ? IOSTemplates.contentsJsonDark : IOSTemplates.contentsJson; await fs3.writeFile(path3.join(launchImageFolder, "Contents.json"), contentsJson); const brandingPath = config.brandingIos || config.branding; if (brandingPath) { const brandingFolder = this.flavorHelper.iOSAssetsBrandingImageFolder; await fs3.ensureDir(brandingFolder); const hasDarkBranding = !!(config.brandingDarkIos || config.brandingDark); const brandingContentsJson = hasDarkBranding ? IOSTemplates.brandingContentsJsonDark : IOSTemplates.brandingContentsJson; await fs3.writeFile(path3.join(brandingFolder, "Contents.json"), brandingContentsJson); } } async generateBackgrounds(config) { const color = config.colorIos || config.color; const darkColor = config.darkColorIos || config.darkColor; const backgroundImage = config.backgroundImageIos || config.backgroundImage; const darkBackgroundImage = config.darkBackgroundImageIos || config.darkBackgroundImage; const backgroundFolder = this.flavorHelper.iOSAssetsLaunchBackgroundFolder; await fs3.ensureDir(backgroundFolder); if (backgroundImage) { await ImageProcessor.copyAndConvertImage( backgroundImage, path3.join(backgroundFolder, "background.png"), "png" ); } else if (color) { await ImageProcessor.createSolidColorImage( color, 1, 1, path3.join(backgroundFolder, "background.png") ); } if (darkBackgroundImage) { await ImageProcessor.copyAndConvertImage( darkBackgroundImage, path3.join(backgroundFolder, "darkbackground.png"), "png" ); } else if (darkColor) { await ImageProcessor.createSolidColorImage( darkColor, 1, 1, path3.join(backgroundFolder, "darkbackground.png") ); } const hasDarkBackground = !!(darkColor || darkBackgroundImage); const backgroundContentsJson = hasDarkBackground ? IOSTemplates.launchBackgroundDarkJson : IOSTemplates.launchBackgroundJson; await fs3.writeFile(path3.join(backgroundFolder, "Contents.json"), backgroundContentsJson); } async updateLaunchScreenStoryboard(config) { const storyboardPath = this.flavorHelper.iOSLaunchScreenStoryboardFile; console.log(`[iOS] Updating ${storyboardPath}`); await fs3.ensureDir(path3.dirname(storyboardPath)); const imagePath = config.imageIos || config.image; const brandingPath = config.brandingIos || config.branding; const contentMode = config.iosContentMode || "center"; const brandingContentMode = config.iosBrandingContentMode || "bottom"; const brandingPadding = config.brandingBottomPaddingIos || config.brandingBottomPadding || 0; if (!await fs3.pathExists(storyboardPath)) { const storyboardContent = IOSTemplates.launchScreenStoryboard.replace( /\[LAUNCH_IMAGE_PLACEHOLDER\]/g, this.flavorHelper.iOSLaunchImageName ); await fs3.writeFile(storyboardPath, storyboardContent); } await this.updateStoryboardContent( storyboardPath, imagePath, brandingPath, contentMode, brandingContentMode, brandingPadding ); } async updateStoryboardContent(storyboardPath, imagePath, brandingPath, contentMode = "center", brandingContentMode = "bottom", brandingPadding = 0) { const content = await fs3.readFile(storyboardPath, "utf-8"); parseString2(content, async (err, result) => { if (err) { console.error("Error parsing storyboard:", err); return; } const document = result.document; const scenes = document.scenes[0].scene; const viewController = scenes[0].objects[0].viewController[0]; const view = viewController.view[0]; if (view.subviews && view.subviews[0].imageView) { const imageViews = view.subviews[0].imageView; const launchImageView = imageViews.find( (iv) => iv.$.image === this.flavorHelper.iOSLaunchImageName ); if (launchImageView) { launchImageView.$.contentMode = contentMode; } if (brandingPath && brandingContentMode !== contentMode) { const brandingImageView = imageViews.find( (iv) => iv.$.image === this.flavorHelper.iOSBrandingImageName ); if (!brandingImageView) { const brandingSubview = IOSTemplates.brandingSubview.replace( "[BRANDING_IMAGE_PLACEHOLDER]", this.flavorHelper.iOSBrandingImageName ); parseString2(`<root>${brandingSubview}</root>`, (err2, brandingResult) => { if (!err2 && brandingResult.root.imageView) { imageViews.push(brandingResult.root.imageView[0]); } }); } if (brandingImageView) { brandingImageView.$.contentMode = brandingContentMode; } } } if (brandingPath && view.constraints) { let constraintsXml = IOSTemplates.brandingCenterBottomConstraints; if (brandingContentMode === "bottomLeft") { constraintsXml = IOSTemplates.brandingLeftBottomConstraints; } else if (brandingContentMode === "bottomRight") { constraintsXml = IOSTemplates.brandingRightBottomConstraints; } constraintsXml = constraintsXml.replace("{bottom_padding}", brandingPadding.toString()); parseString2(`<root>${constraintsXml}</root>`, (err2, constraintsResult) => { if (!err2 && constraintsResult.root.constraints) { if (!view.constraints[0].constraint) { view.constraints[0].constraint = []; } view.constraints[0].constraint.push( ...constraintsResult.root.constraints[0].constraint ); } }); } if (!view.constraints || !view.constraints[0].constraint) { parseString2(`<root>${IOSTemplates.launchBackgroundConstraints}</root>`, (err2, bgResult) => { if (!err2 && bgResult.root.constraints) { view.constraints = bgResult.root.constraints; } }); } if (imagePath && document.resources && document.resources[0].image) { const { width, height } = await ImageProcessor.getImageDimensions(imagePath); const launchImageResource = document.resources[0].image.find( (img) => img.$.name === this.flavorHelper.iOSLaunchImageName ); if (launchImageResource) { launchImageResource.$.width = width.toString(); launchImageResource.$.height = height.toString(); } } const builder = new Builder2({ headless: true, renderOpts: { pretty: true, indent: " " } }); const xml = builder.buildObject(result); await fs3.writeFile( storyboardPath, `<?xml version="1.0" encoding="UTF-8" standalone="no"?> ${xml} ` ); }); } async updateInfoPlistFiles(config) { const plistFiles = config.infoPlistFiles || [this.flavorHelper.iOSInfoPlistFile]; const fullscreen = config.fullscreen || false; for (const plistFile of plistFiles) { if (!await fs3.pathExists(plistFile)) { console.warn(`[iOS] Info.plist file not found: ${plistFile}`); continue; } console.log(`[iOS] Updating ${plistFile}`); await this.updateInfoPlist(plistFile, fullscreen); } } async updateInfoPlist(plistFile, fullscreen) { const content = await fs3.readFile(plistFile, "utf-8"); const plistData = plist.parse(content); plistData.UIStatusBarHidden = fullscreen; if (fullscreen) { plistData.UIViewControllerBasedStatusBarAppearance = false; } const updatedContent = plist.build(plistData); await fs3.writeFile(plistFile, updatedContent); } async removeSplash() { console.log("[iOS] Removing splash screen..."); const foldersToRemove = [ this.flavorHelper.iOSAssetsLaunchImageFolder, this.flavorHelper.iOSAssetsBrandingImageFolder, this.flavorHelper.iOSAssetsLaunchBackgroundFolder ]; for (const folder of foldersToRemove) { if (await fs3.pathExists(folder)) { await fs3.remove(folder); } } console.log("[iOS] Splash screen removal complete!"); } }; // src/platforms/web.ts import fs4 from "fs-extra"; import path4 from "path"; var WebPlatform = class { constructor(flavorHelper) { this.flavorHelper = flavorHelper; } async generateSplash(config) { console.log("[Web] Generating splash screen..."); await this.generateImages(config); await this.generateBackgrounds(config); await this.updateIndexHtml(config); console.log("[Web] Splash screen generation complete!"); } async generateImages(config) { const imagePath = config.imageWeb || config.image; const darkImagePath = config.darkImageWeb || config.darkImage; const brandingPath = config.brandingWeb || config.branding; const brandingDarkPath = config.brandingDarkWeb || config.brandingDark; await fs4.ensureDir(this.flavorHelper.webSplashImagesFolder); if (imagePath) { await this.generateImageSet(imagePath, "light"); } if (darkImagePath) { await this.generateImageSet(darkImagePath, "dark"); } if (brandingPath) { await this.generateImageSet(brandingPath, "branding-light"); } if (brandingDarkPath) { await this.generateImageSet(brandingDarkPath, "branding-dark"); } } async generateImageSet(imagePath, prefix) { const templates = ImageProcessor.getWebTemplates(prefix); await ImageProcessor.processImage( imagePath, templates, this.flavorHelper.webSplashImagesFolder ); } async generateBackgrounds(config) { const color = config.colorWeb || config.color; const darkColor = config.darkColorWeb || config.darkColor; const backgroundImage = config.backgroundImageWeb || config.backgroundImage; const darkBackgroundImage = config.darkBackgroundImageWeb || config.darkBackgroundImage; const backgroundFolder = path4.join(this.flavorHelper.webSplashImagesFolder, "background"); await fs4.ensureDir(backgroundFolder); if (backgroundImage) { await ImageProcessor.copyAndConvertImage( backgroundImage, path4.join(backgroundFolder, "light.png"), "png" ); } else if (color) { await ImageProcessor.createSolidColorImage( color, 1, 1, path4.join(backgroundFolder, "light.png") ); } if (darkBackgroundImage) { await ImageProcessor.copyAndConvertImage( darkBackgroundImage, path4.join(backgroundFolder, "dark.png"), "png" ); } else if (darkColor) { await ImageProcessor.createSolidColorImage( darkColor, 1, 1, path4.join(backgroundFolder, "dark.png") ); } } async updateIndexHtml(config) { const indexPath = this.flavorHelper.webIndexFile; console.log(`[Web] Updating ${indexPath}`); if (!await fs4.pathExists(indexPath)) { console.warn("[Web] index.html not found, skipping update"); return; } const content = await fs4.readFile(indexPath, "utf-8"); const hasImage = !!(config.imageWeb || config.image); const hasDarkImage = !!(config.darkImageWeb || config.darkImage); const hasBranding = !!(config.brandingWeb || config.branding); const hasDarkBranding = !!(config.brandingDarkWeb || config.brandingDark); const imageMode = config.webImageMode || "contain"; const brandingMode = config.brandingMode || "bottom"; const brandingPadding = config.brandingBottomPadding || 0; const styles = this.generateSplashStyles( config, hasImage, hasDarkImage, hasBranding, hasDarkBranding, imageMode, brandingMode, brandingPadding ); let updatedContent; if (content.includes("</head>")) { updatedContent = content.replace("</head>", `${styles} </head>`); } else { updatedContent = `${styles} ${content}`; } await fs4.writeFile(indexPath, updatedContent); } generateSplashStyles(config, hasImage, hasDarkImage, hasBranding, hasDarkBranding, imageMode, brandingMode, brandingPadding) { const color = config.colorWeb || config.color; const darkColor = config.darkColorWeb || config.darkColor; const fullscreen = config.fullscreen || false; const styles = ` <style id="splash-screen-styles"> #splash-screen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background-color: ${color ? `#${color}` : "#ffffff"}; z-index: 9999; ${fullscreen ? "height: 100vh;" : ""} } #splash-screen.hidden { opacity: 0; transition: opacity 0.5s ease-in-out; pointer-events: none; } ${hasImage ? this.generateImageStyles(imageMode) : ""} ${hasBranding ? this.generateBrandingStyles(brandingMode, brandingPadding) : ""} @media (prefers-color-scheme: dark) { #splash-screen { background-color: ${darkColor ? `#${darkColor}` : "#000000"}; } ${hasDarkImage ? this.generateDarkImageStyles() : ""} ${hasDarkBranding ? this.generateDarkBrandingStyles() : ""} } </style> <div id="splash-screen"> ${hasImage ? this.generateImageHtml() : ""} ${hasBranding ? this.generateBrandingHtml() : ""} </div> <script> window.addEventListener('load', function() { setTimeout(function() { var splash = document.getElementById('splash-screen'); if (splash) { splash.classList.add('hidden'); setTimeout(function() { splash.remove(); }, 500); } }, 500); }); </script>`; return styles; } generateImageStyles(imageMode) { return ` #splash-screen-image { max-width: 100%; max-height: 100%; object-fit: ${imageMode}; } #splash-screen-image[src*="light"] { display: block; } #splash-screen-image[src*="dark"] { display: none; } `; } generateDarkImageStyles() { return ` #splash-screen-image[src*="light"] { display: none; } #splash-screen-image[src*="dark"] { display: block; } `; } generateBrandingStyles(mode, padding) { const position = mode === "bottom" ? "bottom: 0;" : mode === "bottomLeft" ? "bottom: 0; left: 0;" : mode === "bottomRight" ? "bottom: 0; right: 0;" : "position: relative;"; return ` #splash-screen-branding { position: absolute; ${position} max-width: 50%; padding-bottom: ${padding}px; } #splash-screen-branding[src*="branding-light"] { display: block; } #splash-screen-branding[src*="branding-dark"] { display: none; } `; } generateDarkBrandingStyles() { return ` #splash-screen-branding[src*="branding-light"] { display: none; } #splash-screen-branding[src*="branding-dark"] { display: block; } `; } generateImageHtml() { const sources = [1, 2, 3, 4].map((density) => `${this.flavorHelper.webSplashImagesFolder}light-${density}x.png ${density}x`).join(", "); const darkSources = [1, 2, 3, 4].map((density) => `${this.flavorHelper.webSplashImagesFolder}dark-${density}x.png ${density}x`).join(", "); return ` <img id="splash-screen-image" src="${this.flavorHelper.webSplashImagesFolder}light-1x.png" srcset="${sources}" alt="Splash Screen" /> <img id="splash-screen-image" src="${this.flavorHelper.webSplashImagesFolder}dark-1x.png" srcset="${darkSources}" alt="Splash Screen Dark" style="display: none;" />`; } generateBrandingHtml() { const sources = [1, 2, 3, 4].map( (density) => `${this.flavorHelper.webSplashImagesFolder}branding-light-${density}x.png ${density}x` ).join(", "); const darkSources = [1, 2, 3, 4].map( (density) => `${this.flavorHelper.webSplashImagesFolder}branding-dark-${density}x.png ${density}x` ).join(", "); return ` <img id="splash-screen-branding" src="${this.flavorHelper.webSplashImagesFolder}branding-light-1x.png" srcset="${sources}" alt="Branding" /> <img id="splash-screen-branding" src="${this.flavorHelper.webSplashImagesFolder}branding-dark-1x.png" srcset="${darkSources}" alt="Branding Dark" style="display: none;" />`; } async removeSplash() { console.log("[Web] Removing splash screen..."); const splashFolder = this.flavorHelper.webSplashFolder; if (await fs4.pathExists(splashFolder)) { await fs4.remove(splashFolder); } const indexPath = this.flavorHelper.webIndexFile; if (await fs4.pathExists(indexPath)) { let content = await fs4.readFile(indexPath, "utf-8"); content = content.replace(/<style id="splash-screen-styles">[\s\S]*?<\/style>/gm, ""); content = content.replace(/<div id="splash-screen">[\s\S]*?<\/div>/gm, ""); content = content.replace( /<script>[\s\S]*?window\.addEventListener\('load'[\s\S]*?<\/script>/gm, "" ); await fs4.writeFile(indexPath, content); } console.log("[Web] Splash screen removal complete!"); } }; // src/core/config.ts import fs6 from "fs-extra"; import path6 from "path"; import yaml from "yaml"; // src/utils/validation.ts import fs5 from "fs-extra"; import path5 from "path"; var ValidationError = class extends Error { constructor(message) { super(message); this.name = "ValidationError"; } }; async function validateConfig(config) { await validateImagePath(config.image, "image"); await validateImagePath(config.imageAndroid, "imageAndroid"); await validateImagePath(config.imageIos, "imageIos"); await validateImagePath(config.imageWeb, "imageWeb"); await validateImagePath(config.darkImage, "darkImage"); await validateImagePath(config.darkImageAndroid, "darkImageAndroid"); await validateImagePath(config.darkImageIos, "darkImageIos"); await validateImagePath(config.darkImageWeb, "darkImageWeb"); await validateImagePath(config.branding, "branding"); await validateImagePath(config.brandingAndroid, "brandingAndroid"); await validateImagePath(config.brandingIos, "brandingIos"); await validateImagePath(config.brandingWeb, "brandingWeb"); await validateImagePath(config.brandingDark, "brandingDark"); await validateImagePath(config.brandingDarkAndroid, "brandingDarkAndroid"); await validateImagePath(config.brandingDarkIos, "brandingDarkIos"); await validateImagePath(config.brandingDarkWeb, "brandingDarkWeb"); await validateImagePath(config.backgroundImage, "backgroundImage"); await validateImagePath(config.backgroundImageAndroid, "backgroundImageAndroid"); await validateImagePath(config.backgroundImageIos, "backgroundImageIos"); await validateImagePath(config.backgroundImageWeb, "backgroundImageWeb"); await validateImagePath(config.darkBackgroundImage, "darkBackgroundImage"); await validateImagePath(config.darkBackgroundImageAndroid, "darkBackgroundImageAndroid"); await validateImagePath(config.darkBackgroundImageIos, "darkBackgroundImageIos"); await validateImagePath(config.darkBackgroundImageWeb, "darkBackgroundImageWeb");