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,347 lines (1,326 loc) 65.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { SplashGenerator: () => SplashGenerator, SplashScreen: () => SplashScreen, generateFlavorSplashes: () => generateFlavorSplashes, generateSplash: () => generateSplash, removeSplash: () => removeSplash }); module.exports = __toCommonJS(index_exports); // src/platforms/android.ts var import_fs_extra2 = __toESM(require("fs-extra"), 1); var import_path2 = __toESM(require("path"), 1); var import_xml2js = require("xml2js"); // src/utils/image.ts var import_sharp = __toESM(require("sharp"), 1); var import_fs_extra = __toESM(require("fs-extra"), 1); var import_path = __toESM(require("path"), 1); var ImageProcessor = class { static async processImage(sourcePath, templates, baseOutputPath, options = {}) { const sourceImage = (0, import_sharp.default)(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 ? import_path.default.join(baseOutputPath, template.directory) : baseOutputPath; const outputPath = import_path.default.join(outputDir, template.fileName); await import_fs_extra.default.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 import_fs_extra.default.ensureDir(import_path.default.dirname(outputPath)); const { r, g, b } = this.hexToRgb(color); await (0, import_sharp.default)({ create: { width, height, channels: 3, background: { r, g, b } } }).png().toFile(outputPath); } static async copyAndConvertImage(sourcePath, outputPath, targetFormat = "png") { await import_fs_extra.default.ensureDir(import_path.default.dirname(outputPath)); const sourceExt = import_path.default.extname(sourcePath).toLowerCase(); const targetExt = import_path.default.extname(outputPath).toLowerCase(); if (sourceExt === `.${targetFormat}` && sourceExt === targetExt) { await import_fs_extra.default.copy(sourcePath, outputPath); return; } let processor = (0, import_sharp.default)(sourcePath); switch (targetFormat) { case "png": processor = processor.png(); break; case "jpg": processor = processor.jpeg(); break; case "gif": if (sourceExt === ".gif") { await import_fs_extra.default.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 (0, import_sharp.default)(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, import_path2.default.join(this.flavorHelper.androidDrawableFolder, "background.png") ); if (darkColor || darkBackgroundImage) { await this.createBackground( darkColor, darkBackgroundImage, import_path2.default.join(this.flavorHelper.androidDrawableNightFolder, "background.png") ); } await this.createBackground( color, backgroundImage, import_path2.default.join(this.flavorHelper.androidMainResFolder, "drawable-v21/background.png") ); if (darkColor || darkBackgroundImage) { await this.createBackground( darkColor, darkBackgroundImage, import_path2.default.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 import_fs_extra2.default.ensureDir(import_path2.default.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 import_fs_extra2.default.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 import_fs_extra2.default.ensureDir(import_path2.default.dirname(filePath)); if (!await import_fs_extra2.default.pathExists(filePath)) { await import_fs_extra2.default.writeFile(filePath, template); } const content = await import_fs_extra2.default.readFile(filePath, "utf-8"); (0, import_xml2js.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 import_xml2js.Builder({ headless: true }); const xml = builder.buildObject(result); await import_fs_extra2.default.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 import_fs_extra2.default.pathExists(manifestPath)) { console.warn("[Android] AndroidManifest.xml not found, skipping orientation update"); return; } console.log("[Android] Updating AndroidManifest.xml orientation..."); const content = await import_fs_extra2.default.readFile(manifestPath, "utf-8"); (0, import_xml2js.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 import_xml2js.Builder({ headless: true }); const xml = builder.buildObject(result); await import_fs_extra2.default.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 = import_path2.default.join(this.flavorHelper.androidMainResFolder, folder); if (await import_fs_extra2.default.pathExists(folderPath)) { const splashFile = import_path2.default.join(folderPath, "splash.png"); const brandingFile = import_path2.default.join(folderPath, "branding.png"); if (await import_fs_extra2.default.pathExists(splashFile)) { await import_fs_extra2.default.remove(splashFile); } if (await import_fs_extra2.default.pathExists(brandingFile)) { await import_fs_extra2.default.remove(brandingFile); } } } console.log("[Android] Splash screen removal complete!"); } }; // src/platforms/ios.ts var import_fs_extra3 = __toESM(require("fs-extra"), 1); var import_path3 = __toESM(require("path"), 1); var import_plist = __toESM(require("plist"), 1); var import_xml2js2 = require("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 import_fs_extra3.default.ensureDir(outputFolder); const templates = ImageProcessor.getIOSTemplates(baseName, false); for (const template of templates) { await ImageProcessor.createSolidColorImage( "ffffff", 1, 1, import_path3.default.join(outputFolder, template.fileName) ); } } async createContentsJson(config) { const launchImageFolder = this.flavorHelper.iOSAssetsLaunchImageFolder; await import_fs_extra3.default.ensureDir(launchImageFolder); const hasDarkImage = !!(config.darkImageIos || config.darkImage); const contentsJson = hasDarkImage ? IOSTemplates.contentsJsonDark : IOSTemplates.contentsJson; await import_fs_extra3.default.writeFile(import_path3.default.join(launchImageFolder, "Contents.json"), contentsJson); const brandingPath = config.brandingIos || config.branding; if (brandingPath) { const brandingFolder = this.flavorHelper.iOSAssetsBrandingImageFolder; await import_fs_extra3.default.ensureDir(brandingFolder); const hasDarkBranding = !!(config.brandingDarkIos || config.brandingDark); const brandingContentsJson = hasDarkBranding ? IOSTemplates.brandingContentsJsonDark : IOSTemplates.brandingContentsJson; await import_fs_extra3.default.writeFile(import_path3.default.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 import_fs_extra3.default.ensureDir(backgroundFolder); if (backgroundImage) { await ImageProcessor.copyAndConvertImage( backgroundImage, import_path3.default.join(backgroundFolder, "background.png"), "png" ); } else if (color) { await ImageProcessor.createSolidColorImage( color, 1, 1, import_path3.default.join(backgroundFolder, "background.png") ); } if (darkBackgroundImage) { await ImageProcessor.copyAndConvertImage( darkBackgroundImage, import_path3.default.join(backgroundFolder, "darkbackground.png"), "png" ); } else if (darkColor) { await ImageProcessor.createSolidColorImage( darkColor, 1, 1, import_path3.default.join(backgroundFolder, "darkbackground.png") ); } const hasDarkBackground = !!(darkColor || darkBackgroundImage); const backgroundContentsJson = hasDarkBackground ? IOSTemplates.launchBackgroundDarkJson : IOSTemplates.launchBackgroundJson; await import_fs_extra3.default.writeFile(import_path3.default.join(backgroundFolder, "Contents.json"), backgroundContentsJson); } async updateLaunchScreenStoryboard(config) { const storyboardPath = this.flavorHelper.iOSLaunchScreenStoryboardFile; console.log(`[iOS] Updating ${storyboardPath}`); await import_fs_extra3.default.ensureDir(import_path3.default.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 import_fs_extra3.default.pathExists(storyboardPath)) { const storyboardContent = IOSTemplates.launchScreenStoryboard.replace( /\[LAUNCH_IMAGE_PLACEHOLDER\]/g, this.flavorHelper.iOSLaunchImageName ); await import_fs_extra3.default.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 import_fs_extra3.default.readFile(storyboardPath, "utf-8"); (0, import_xml2js2.parseString)(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 ); (0, import_xml2js2.parseString)(`<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()); (0, import_xml2js2.parseString)(`<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) { (0, import_xml2js2.parseString)(`<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 import_xml2js2.Builder({ headless: true, renderOpts: { pretty: true, indent: " " } }); const xml = builder.buildObject(result); await import_fs_extra3.default.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 import_fs_extra3.default.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 import_fs_extra3.default.readFile(plistFile, "utf-8"); const plistData = import_plist.default.parse(content); plistData.UIStatusBarHidden = fullscreen; if (fullscreen) { plistData.UIViewControllerBasedStatusBarAppearance = false; } const updatedContent = import_plist.default.build(plistData); await import_fs_extra3.default.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 import_fs_extra3.default.pathExists(folder)) { await import_fs_extra3.default.remove(folder); } } console.log("[iOS] Splash screen removal complete!"); } }; // src/platforms/web.ts var import_fs_extra4 = __toESM(require("fs-extra"), 1); var import_path4 = __toESM(require("path"), 1); 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 import_fs_extra4.default.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 = import_path4.default.join(this.flavorHelper.webSplashImagesFolder, "background"); await import_fs_extra4.default.ensureDir(backgroundFolder); if (backgroundImage) { await ImageProcessor.copyAndConvertImage( backgroundImage, import_path4.default.join(backgroundFolder, "light.png"), "png" ); } else if (color) { await ImageProcessor.createSolidColorImage( color, 1, 1, import_path4.default.join(backgroundFolder, "light.png") ); } if (darkBackgroundImage) { await ImageProcessor.copyAndConvertImage( darkBackgroundImage, import_path4.default.join(backgroundFolder, "dark.png"), "png" ); } else if (darkColor) { await ImageProcessor.createSolidColorImage( darkColor, 1, 1, import_path4.default.join(backgroundFolder, "dark.png") ); } } async updateIndexHtml(config) { const indexPath = this.flavorHelper.webIndexFile; console.log(`[Web] Updating ${indexPath}`); if (!await import_fs_extra4.default.pathExists(indexPath)) { console.warn("[Web] index.html not found, skipping update"); return; } const content = await import_fs_extra4.default.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 import_fs_extra4.default.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