@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
JavaScript
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");