vxrn 2.0.0-beta.129.1 → 2.0.0-beta.130.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,8 @@ import { swiftPackageDirectories } from "../utils/swiftPackageId.mjs";
10
10
  const nativeProjectPatches = module.createRequire(import.meta.url)("../../native-project-patches.cjs");
11
11
  const notificationsHost = nativeProjectPatches.ONE_NOTIFICATIONS;
12
12
  const updatesHost = nativeProjectPatches.ONE_UPDATES;
13
+ const PORTRAIT_ORIENTATIONS = ["UIInterfaceOrientationPortrait", "UIInterfaceOrientationPortraitUpsideDown"];
14
+ const LANDSCAPE_ORIENTATIONS = ["UIInterfaceOrientationLandscapeLeft", "UIInterfaceOrientationLandscapeRight"];
13
15
  const validatePrebuildApp = validateNativeApp;
14
16
  const ANDROID_DENSITIES = {
15
17
  mdpi: 1,
@@ -964,6 +966,46 @@ async function generateAppIcons(args) {
964
966
  }
965
967
  }
966
968
  }
969
+ async function generateAdaptiveIcon(args) {
970
+ const { root, dest, app } = args;
971
+ const adaptiveIcon = app.android?.adaptiveIcon;
972
+ if (!adaptiveIcon) return;
973
+ const res = path.join(dest, "app", "src", "main", "res");
974
+ const layers = {
975
+ foreground: adaptiveIcon.foreground,
976
+ background: adaptiveIcon.background,
977
+ monochrome: adaptiveIcon.monochrome
978
+ };
979
+ for (const [layer, file] of Object.entries(layers)) {
980
+ if (!file) continue;
981
+ const source = path.resolve(root, file);
982
+ if (!FSExtra.existsSync(source)) {
983
+ throw new Error(`[vxrn] native.app.android.adaptiveIcon.${layer} does not exist: ${source}`);
984
+ }
985
+ for (const [density, multiplier] of Object.entries(ANDROID_DENSITIES)) {
986
+ const pixels = 108 * multiplier;
987
+ await sharp(source).rotate().resize(pixels, pixels, { fit: "cover" }).png().toFile(path.join(res, `mipmap-${density}`, `ic_launcher_${layer}.png`));
988
+ }
989
+ }
990
+ if (!adaptiveIcon.background) {
991
+ FSExtra.writeFileSync(path.join(res, "values", "ic_launcher_background.xml"), `<?xml version="1.0" encoding="utf-8"?>
992
+ <resources>
993
+ <color name="ic_launcher_background">${adaptiveIcon.backgroundColor ?? "#FFFFFF"}</color>
994
+ </resources>
995
+ `);
996
+ }
997
+ const adaptiveXml = `<?xml version="1.0" encoding="utf-8"?>
998
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
999
+ <background android:drawable="${adaptiveIcon.background ? "@mipmap/ic_launcher_background" : "@color/ic_launcher_background"}"/>
1000
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
1001
+ ${adaptiveIcon.monochrome ? " <monochrome android:drawable=\"@mipmap/ic_launcher_monochrome\"/>\n" : ""}</adaptive-icon>
1002
+ `;
1003
+ const anydpi = path.join(res, "mipmap-anydpi-v26");
1004
+ FSExtra.mkdirSync(anydpi, { recursive: true });
1005
+ for (const filename of ["ic_launcher.xml", "ic_launcher_round.xml"]) {
1006
+ FSExtra.writeFileSync(path.join(anydpi, filename), adaptiveXml);
1007
+ }
1008
+ }
967
1009
  async function generateSplashScreen(args) {
968
1010
  const { root, dest, platform, app } = args;
969
1011
  if (!app.splash) return;
@@ -971,7 +1013,9 @@ async function generateSplashScreen(args) {
971
1013
  if (!FSExtra.existsSync(source)) {
972
1014
  throw new Error(`[vxrn] native.app.splash source does not exist: ${source}`);
973
1015
  }
974
- const { data: artwork, info: metadata } = await sharp(source).rotate().trim({ background: app.splash.backgroundColor }).png().toBuffer({ resolveWithObject: true });
1016
+ const cover = app.splash.resizeMode === "cover";
1017
+ const image = sharp(source).rotate();
1018
+ const { data: artwork, info: metadata } = await (cover ? image : image.trim({ background: app.splash.backgroundColor })).png().toBuffer({ resolveWithObject: true });
975
1019
  if (!metadata.width || !metadata.height) {
976
1020
  throw new Error("[vxrn] native.app.splash source must be an image");
977
1021
  }
@@ -1012,14 +1056,17 @@ async function generateSplashScreen(args) {
1012
1056
  <view key="view" contentMode="scaleToFill" id="launch-view">
1013
1057
  <rect key="frame" x="0.0" y="0.0" width="390" height="844"/>
1014
1058
  <subviews>
1015
- <imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" image="Splash" translatesAutoresizingMaskIntoConstraints="NO" id="splash-image"/>
1059
+ <imageView userInteractionEnabled="NO" contentMode="${cover ? "scaleAspectFill" : "scaleAspectFit"}" image="Splash" translatesAutoresizingMaskIntoConstraints="NO" id="splash-image"/>
1016
1060
  </subviews>
1017
1061
  <color key="backgroundColor" red="${red}" green="${green}" blue="${blue}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
1018
1062
  <constraints>
1019
- <constraint firstItem="splash-image" firstAttribute="centerX" secondItem="launch-view" secondAttribute="centerX" id="splash-center-x"/>
1063
+ ${cover ? ` <constraint firstItem="splash-image" firstAttribute="leading" secondItem="launch-view" secondAttribute="leading" id="splash-leading"/>
1064
+ <constraint firstItem="splash-image" firstAttribute="trailing" secondItem="launch-view" secondAttribute="trailing" id="splash-trailing"/>
1065
+ <constraint firstItem="splash-image" firstAttribute="top" secondItem="launch-view" secondAttribute="top" id="splash-top"/>
1066
+ <constraint firstItem="splash-image" firstAttribute="bottom" secondItem="launch-view" secondAttribute="bottom" id="splash-bottom"/>` : ` <constraint firstItem="splash-image" firstAttribute="centerX" secondItem="launch-view" secondAttribute="centerX" id="splash-center-x"/>
1020
1067
  <constraint firstItem="splash-image" firstAttribute="centerY" secondItem="launch-view" secondAttribute="centerY" id="splash-center-y"/>
1021
1068
  <constraint firstItem="splash-image" firstAttribute="width" constant="${artworkWidth}" id="splash-width"/>
1022
- <constraint firstItem="splash-image" firstAttribute="height" constant="${artworkHeight}" id="splash-height"/>
1069
+ <constraint firstItem="splash-image" firstAttribute="height" constant="${artworkHeight}" id="splash-height"/>`}
1023
1070
  </constraints>
1024
1071
  </view>
1025
1072
  </viewController>
@@ -1160,6 +1207,21 @@ ${schemes.map((scheme) => ` <string>${scheme}</string>`).join("\n")}
1160
1207
  }
1161
1208
  stamps.push(` <key>${updatesHost.runtimeVersionInfoPlistKey}</key>
1162
1209
  <string>${escapeXml(app.updates.runtimeVersion)}</string>`);
1210
+ }
1211
+ if (app.userInterfaceStyle !== void 0) {
1212
+ stamps.push(` <key>UIUserInterfaceStyle</key>
1213
+ <string>${app.userInterfaceStyle === "light" ? "Light" : app.userInterfaceStyle === "dark" ? "Dark" : "Automatic"}</string>`);
1214
+ }
1215
+ if (app.orientation !== void 0) {
1216
+ const phoneOrientations = app.orientation === "portrait" ? PORTRAIT_ORIENTATIONS : app.orientation === "landscape" ? LANDSCAPE_ORIENTATIONS : [...PORTRAIT_ORIENTATIONS, ...LANDSCAPE_ORIENTATIONS];
1217
+ const phoneList = /\t<key>UISupportedInterfaceOrientations<\/key>\n\t<array>\n[\s\S]*?\t<\/array>/;
1218
+ if (!phoneList.test(rendered)) {
1219
+ throw new Error(`[vxrn] prebuild template ${relativePath} lost its UISupportedInterfaceOrientations list`);
1220
+ }
1221
+ rendered = rendered.replace(phoneList, ` <key>UISupportedInterfaceOrientations</key>
1222
+ <array>
1223
+ ${phoneOrientations.map((orientation) => ` <string>${orientation}</string>`).join("\n")}
1224
+ </array>`);
1163
1225
  }
1164
1226
  if (stamps.length) {
1165
1227
  const anchor = " <key>LSRequiresIPhoneOS</key>";
@@ -1265,6 +1327,21 @@ ${line}
1265
1327
  `;
1266
1328
  }
1267
1329
  }
1330
+ if (platform === "android" && relativePath === "app/src/main/res/values/styles.xml" && (app.userInterfaceStyle === "light" || app.userInterfaceStyle === "dark")) {
1331
+ const dayNight = "parent=\"Theme.AppCompat.DayNight.NoActionBar\"";
1332
+ if (!rendered.includes(dayNight)) {
1333
+ throw new Error(`[vxrn] cannot lock userInterfaceStyle: expected ${dayNight} in app/src/main/res/values/styles.xml`);
1334
+ }
1335
+ rendered = rendered.replace(dayNight, app.userInterfaceStyle === "light" ? "parent=\"Theme.AppCompat.Light.NoActionBar\"" : "parent=\"Theme.AppCompat.NoActionBar\"");
1336
+ }
1337
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.orientation !== void 0) {
1338
+ const anchor = "android:name=\".MainActivity\"";
1339
+ if (!rendered.includes(anchor)) {
1340
+ throw new Error("[vxrn] cannot stamp the orientation: expected .MainActivity in app/src/main/AndroidManifest.xml");
1341
+ }
1342
+ rendered = rendered.replace(anchor, `${anchor}
1343
+ android:screenOrientation="${app.orientation === "default" ? "unspecified" : app.orientation}"`);
1344
+ }
1268
1345
  if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.pictureInPicture) {
1269
1346
  const anchor = "android:name=\".MainActivity\"";
1270
1347
  if (!rendered.includes(anchor)) {
@@ -1474,6 +1551,11 @@ const generateForPlatform = async (root, platform, app, outDir = path.resolve(ro
1474
1551
  platform,
1475
1552
  app
1476
1553
  });
1554
+ if (platform === "android") await generateAdaptiveIcon({
1555
+ root,
1556
+ dest,
1557
+ app
1558
+ });
1477
1559
  await generateSplashScreen({
1478
1560
  root,
1479
1561
  dest,