expo-splash-screen2 1.2.0 → 1.3.0

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/plugin/build/index.js +721 -1499
@@ -39,7 +39,6 @@ const fs = __importStar(require("fs"));
39
39
  const path = __importStar(require("path"));
40
40
  const child_process_1 = require("child_process");
41
41
  const android_1 = require("./templates/android");
42
- const ios_1 = require("./templates/ios");
43
42
  const CUSTOM_SPLASH_ACTIVITY_NAME = 'SplashScreen2Activity';
44
43
  const STORYBOARD_FILE_PATH = './SplashScreen.storyboard';
45
44
  const STORYBOARD_MOD_NAME = 'splashScreenStoryboard';
@@ -364,31 +363,6 @@ function createSplashScreenLogoForNormalMode(projectRoot, androidMainPath, image
364
363
  console.error(`[expo-splash-screen2] Error creating splashscreen_logo for normal mode: ${error}`);
365
364
  }
366
365
  }
367
- function createBackgroundDrawable(androidResPath, backgroundColor) {
368
- const drawableDir = path.join(androidResPath, 'res', 'drawable');
369
- if (!fs.existsSync(drawableDir)) {
370
- fs.mkdirSync(drawableDir, { recursive: true });
371
- }
372
- const xmlPath = path.join(drawableDir, 'splash_html_background.xml');
373
- const xmlContent = `<?xml version="1.0" encoding="utf-8"?>
374
- <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
375
- <item>
376
- <color android:color="${backgroundColor}" />
377
- </item>
378
- <item>
379
- <bitmap
380
- android:gravity="center"
381
- android:src="@drawable/splash_icon" />
382
- </item>
383
- </layer-list>
384
- `;
385
- try {
386
- fs.writeFileSync(xmlPath, xmlContent);
387
- }
388
- catch (error) {
389
- console.error(`[expo-splash-screen2] Error creating background drawable: ${error}`);
390
- }
391
- }
392
366
  function removeHashFromFileName(fileName) {
393
367
  const hashPattern = /^(.+)\.([0-9a-f]{32,})\.([^.]+)$/i;
394
368
  const match = fileName.match(hashPattern);
@@ -766,13 +740,6 @@ function modifyMainActivityForImageMode(content, packageName, imageResourceName)
766
740
  console.warn('[expo-splash-screen2] MainActivity class not found');
767
741
  return content;
768
742
  }
769
- const importsToAdd = `
770
- import android.os.Handler
771
- import android.os.Looper
772
- import android.view.View
773
- import android.view.ViewGroup
774
- import android.widget.ImageView
775
- import android.graphics.drawable.Drawable`;
776
743
  let hasImports = content.includes('import android.os.Handler') &&
777
744
  content.includes('import android.os.Looper') &&
778
745
  content.includes('import android.view.View') &&
@@ -844,7 +811,7 @@ import android.graphics.drawable.Drawable`;
844
811
  android.util.Log.e("MainActivity", "Error setting background drawable", e)
845
812
  }
846
813
  }
847
-
814
+
848
815
  // Create ImageView, display .9 patch background
849
816
  // Use FIT_XY scaleType to ensure .9 patch correctly stretches and fills, completely consistent with system splash screen
850
817
  val imageView = ImageView(this).apply {
@@ -885,6 +852,18 @@ import android.graphics.drawable.Drawable`;
885
852
  fun preventAutoHide() {
886
853
  preventAutoHide = true
887
854
  android.util.Log.d("MainActivity", "preventAutoHide called, preventAutoHide: $preventAutoHide")
855
+ // Show splash image view when preventAutoHide is called
856
+ Handler(Looper.getMainLooper()).post {
857
+ setupSplashImageView()
858
+ }
859
+ }
860
+
861
+ override fun onContentChanged() {
862
+ super.onContentChanged()
863
+ // Show splash image view when content changed
864
+ Handler(Looper.getMainLooper()).post {
865
+ setupSplashImageView()
866
+ }
888
867
  }
889
868
 
890
869
  fun hideSplashImageViewContainer(force: Boolean = false) {
@@ -927,21 +906,17 @@ import android.graphics.drawable.Drawable`;
927
906
  }
928
907
  }
929
908
  const onCreateContent = modifiedContent.substring(onCreateIndex, onCreateEndIndex);
930
- if (!onCreateContent.includes('setupSplashImageView')) {
931
- const superOnCreateIndex = onCreateContent.indexOf('super.onCreate');
932
- if (superOnCreateIndex !== -1) {
933
- const superOnCreateEndIndex = onCreateContent.indexOf('\n', superOnCreateIndex);
934
- if (superOnCreateEndIndex !== -1) {
935
- const setupCall = `
936
- // Immediately show background image ImageView container in onCreate
937
- Handler(Looper.getMainLooper()).post {
938
- setupSplashImageView()
939
- }`;
940
- modifiedContent = modifiedContent.substring(0, onCreateIndex + superOnCreateEndIndex + 1) +
941
- setupCall + '\n' +
942
- modifiedContent.substring(onCreateIndex + superOnCreateEndIndex + 1);
943
- }
944
- }
909
+ let cleanedOnCreateContent = onCreateContent;
910
+ const themeCommentRegex = /(\s*\/\/\s*Set the theme to AppTheme[^\n]*\n)+(\s*\/\/\s*[^\n]*\n)*(\s*\/\/\s*This is required for expo-splash-screen[^\n]*\n)?/g;
911
+ cleanedOnCreateContent = cleanedOnCreateContent.replace(themeCommentRegex, '');
912
+ const setThemeRegex = /(\s*\/\/\s*[^\n]*\n)*\s*setTheme\s*\(\s*R\.style\.AppTheme\s*\)\s*;?\s*\n?/g;
913
+ cleanedOnCreateContent = cleanedOnCreateContent.replace(setThemeRegex, '');
914
+ const setupSplashRegex = /(\s*\/\/\s*[^\n]*\n)*\s*Handler\s*\(\s*Looper\.getMainLooper\(\)\s*\)\.post\s*\{[\s\S]*?setupSplashImageView\s*\(\)[\s\S]*?\}\s*/g;
915
+ cleanedOnCreateContent = cleanedOnCreateContent.replace(setupSplashRegex, '');
916
+ if (cleanedOnCreateContent !== onCreateContent) {
917
+ modifiedContent = modifiedContent.substring(0, onCreateIndex) +
918
+ cleanedOnCreateContent +
919
+ modifiedContent.substring(onCreateEndIndex);
945
920
  }
946
921
  }
947
922
  const classIndex = modifiedContent.indexOf(classMatch[0]) + classMatch[0].length;
@@ -1454,21 +1429,55 @@ function modifyMainActivity(content, packageName, backgroundColor) {
1454
1429
  if (!hasWebViewCode || !hasCompanionObject) {
1455
1430
  const classIndex = modifiedContent.indexOf(classMatch[0]) + classMatch[0].length;
1456
1431
  const firstMethodMatch = modifiedContent.substring(classIndex).match(/\s+(override\s+)?fun\s+/);
1457
- if (firstMethodMatch) {
1458
- const insertIndex = classIndex + firstMethodMatch.index;
1459
- modifiedContent = (modifiedContent.substring(0, insertIndex) +
1460
- companionObjectCode +
1461
- webViewCode +
1462
- '\n' +
1463
- modifiedContent.substring(insertIndex));
1464
- }
1465
- else {
1466
- const lastBraceIndex = modifiedContent.lastIndexOf('}');
1467
- modifiedContent = (modifiedContent.substring(0, lastBraceIndex) +
1468
- companionObjectCode +
1469
- webViewCode +
1470
- '\n' +
1471
- modifiedContent.substring(lastBraceIndex));
1432
+ if (!hasWebViewCode) {
1433
+ if (firstMethodMatch) {
1434
+ const insertIndex = classIndex + firstMethodMatch.index;
1435
+ modifiedContent = (modifiedContent.substring(0, insertIndex) +
1436
+ webViewCode +
1437
+ '\n' +
1438
+ modifiedContent.substring(insertIndex));
1439
+ }
1440
+ else {
1441
+ const lastBraceIndex = modifiedContent.lastIndexOf('}');
1442
+ modifiedContent = (modifiedContent.substring(0, lastBraceIndex) +
1443
+ webViewCode +
1444
+ '\n' +
1445
+ modifiedContent.substring(lastBraceIndex));
1446
+ }
1447
+ }
1448
+ if (!hasCompanionObject) {
1449
+ let braceCount = 0;
1450
+ let classStartIndex = modifiedContent.indexOf(classMatch[0]);
1451
+ let foundClassStart = false;
1452
+ let classEndIndex = -1;
1453
+ for (let i = classStartIndex; i < modifiedContent.length; i++) {
1454
+ if (modifiedContent[i] === '{') {
1455
+ braceCount++;
1456
+ foundClassStart = true;
1457
+ }
1458
+ else if (modifiedContent[i] === '}') {
1459
+ braceCount--;
1460
+ if (foundClassStart && braceCount === 0) {
1461
+ classEndIndex = i;
1462
+ break;
1463
+ }
1464
+ }
1465
+ }
1466
+ if (classEndIndex !== -1) {
1467
+ modifiedContent = (modifiedContent.substring(0, classEndIndex) +
1468
+ '\n' +
1469
+ companionObjectCode +
1470
+ '\n' +
1471
+ modifiedContent.substring(classEndIndex));
1472
+ }
1473
+ else {
1474
+ const lastBraceIndex = modifiedContent.lastIndexOf('}');
1475
+ modifiedContent = (modifiedContent.substring(0, lastBraceIndex) +
1476
+ '\n' +
1477
+ companionObjectCode +
1478
+ '\n' +
1479
+ modifiedContent.substring(lastBraceIndex));
1480
+ }
1472
1481
  }
1473
1482
  }
1474
1483
  if (modifiedContent.includes('设置背景色为传入的 backgroundColor')) {
@@ -1482,68 +1491,525 @@ function modifyMainActivity(content, packageName, backgroundColor) {
1482
1491
  return modifiedContent;
1483
1492
  }
1484
1493
  function modifyMainActivityForBlendMode(content, packageName, imageResourceName) {
1494
+ if (content.includes('splashImageViewContainer') && content.includes('setupSplashImageView') &&
1495
+ content.includes('onContentChanged') && content.includes('setupWebViewContainer')) {
1496
+ return content;
1497
+ }
1485
1498
  let modifiedContent = modifyMainActivity(content, packageName, '#ffffff');
1486
- if (modifiedContent.includes('Set .9 patch image as container background')) {
1499
+ const fitsSystemWindowsPattern = /(\s*\/\/\s*Ensure container is not affected by system window insets[^\n]*\n\s*fitsSystemWindows\s*=\s*false\s*)(\s*\/\/\s*Set \.9 patch image as container background[\s\S]*?android\.util\.Log\.e\("MainActivity", "Error setting background drawable", e\)[\s\S]*?\}\s*)(\n\s*\})/;
1500
+ if (fitsSystemWindowsPattern.test(modifiedContent)) {
1501
+ modifiedContent = modifiedContent.replace(fitsSystemWindowsPattern, '$1$3');
1502
+ }
1503
+ const onCreateMatch = modifiedContent.match(/override\s+fun\s+onCreate\s*\([^)]*\)\s*\{/);
1504
+ if (onCreateMatch) {
1505
+ const onCreateIndex = modifiedContent.indexOf(onCreateMatch[0]);
1506
+ let braceCount = 0;
1507
+ let onCreateEndIndex = onCreateIndex + onCreateMatch[0].length;
1508
+ let foundStart = false;
1509
+ for (let i = onCreateIndex; i < modifiedContent.length; i++) {
1510
+ if (modifiedContent[i] === '{') {
1511
+ braceCount++;
1512
+ foundStart = true;
1513
+ }
1514
+ else if (modifiedContent[i] === '}') {
1515
+ braceCount--;
1516
+ if (foundStart && braceCount === 0) {
1517
+ onCreateEndIndex = i + 1;
1518
+ break;
1519
+ }
1520
+ }
1521
+ }
1522
+ const onCreateContent = modifiedContent.substring(onCreateIndex, onCreateEndIndex);
1523
+ let cleanedOnCreateContent = onCreateContent;
1524
+ const setupWebViewRegex = /(\s*\/\/\s*[^\n]*\n)*\s*(WindowCompat\.setDecorFitsSystemWindows|setupWebViewContainer|Handler\s*\(\s*Looper\.getMainLooper\(\)\s*\)\.post)[\s\S]*?\}\s*/g;
1525
+ cleanedOnCreateContent = cleanedOnCreateContent.replace(setupWebViewRegex, '');
1526
+ if (cleanedOnCreateContent !== onCreateContent) {
1527
+ modifiedContent = modifiedContent.substring(0, onCreateIndex) +
1528
+ cleanedOnCreateContent +
1529
+ modifiedContent.substring(onCreateEndIndex);
1530
+ }
1531
+ }
1532
+ const classMatch = modifiedContent.match(/class\s+MainActivity\s*[^:]*:/);
1533
+ if (!classMatch) {
1534
+ console.warn('[expo-splash-screen2] MainActivity class not found');
1487
1535
  return modifiedContent;
1488
1536
  }
1489
- const fitsSystemWindowsPattern = /(\s*\/\/\s*Ensure container is not affected by system window insets[^\n]*\n\s*fitsSystemWindows\s*=\s*false\s*)(\n\s*\})/;
1490
- const backgroundCode = `
1491
- // Set .9 patch image as container background
1537
+ const importsToAdd = [
1538
+ 'import android.os.Handler',
1539
+ 'import android.os.Looper',
1540
+ 'import android.view.View',
1541
+ 'import android.view.ViewGroup',
1542
+ 'import android.widget.ImageView'
1543
+ ];
1544
+ let hasImports = importsToAdd.every(imp => modifiedContent.includes(imp));
1545
+ if (!hasImports) {
1546
+ const missingImports = importsToAdd.filter(imp => !modifiedContent.includes(imp));
1547
+ if (missingImports.length > 0) {
1548
+ const lastImportIndex = modifiedContent.lastIndexOf('import ');
1549
+ if (lastImportIndex !== -1) {
1550
+ const nextLineIndex = modifiedContent.indexOf('\n', lastImportIndex);
1551
+ if (nextLineIndex !== -1) {
1552
+ const missingImportsText = missingImports.join('\n') + '\n';
1553
+ modifiedContent = modifiedContent.substring(0, nextLineIndex + 1) +
1554
+ missingImportsText +
1555
+ modifiedContent.substring(nextLineIndex + 1);
1556
+ }
1557
+ }
1558
+ }
1559
+ }
1560
+ const imageViewCode = `
1561
+ private var splashImageViewContainer: ViewGroup? = null
1562
+ private var preventAutoHideImageView = false
1563
+
1564
+ private fun setupSplashImageView() {
1565
+ try {
1566
+ // If container already exists, return directly
1567
+ if (splashImageViewContainer != null) {
1568
+ android.util.Log.d("MainActivity", "Splash ImageView container already exists")
1569
+ return
1570
+ }
1571
+
1572
+ android.util.Log.d("MainActivity", "Creating splash ImageView container")
1573
+
1574
+ // Create container
1575
+ splashImageViewContainer = object : ViewGroup(this) {
1576
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
1577
+ val childCount = childCount
1578
+ val width = r - l
1579
+ val height = b - t
1580
+ for (i in 0 until childCount) {
1581
+ val child = getChildAt(i)
1582
+ child.layout(0, 0, width, height)
1583
+ }
1584
+ }
1585
+ }.apply {
1586
+ layoutParams = ViewGroup.LayoutParams(
1587
+ ViewGroup.LayoutParams.MATCH_PARENT,
1588
+ ViewGroup.LayoutParams.MATCH_PARENT
1589
+ )
1590
+ // Set background to .9 patch image, ensure consistency with system splash screen
1492
1591
  try {
1493
1592
  val drawable = resources.getDrawable(
1494
1593
  resources.getIdentifier("${imageResourceName}", "drawable", packageName),
1495
1594
  null
1496
1595
  )
1497
- this.background = drawable
1596
+ background = drawable
1498
1597
  } catch (e: Exception) {
1499
1598
  android.util.Log.e("MainActivity", "Error setting background drawable", e)
1500
- }`;
1501
- if (fitsSystemWindowsPattern.test(modifiedContent)) {
1502
- modifiedContent = modifiedContent.replace(fitsSystemWindowsPattern, `$1${backgroundCode}$2`);
1503
- }
1504
- return modifiedContent;
1505
- }
1506
- function generatePrivacyPolicyActivity(packageName, projectRoot, androidMainPath) {
1507
- const javaDir = path.join(androidMainPath, 'java', ...packageName.split('.'));
1508
- if (!fs.existsSync(javaDir)) {
1509
- fs.mkdirSync(javaDir, { recursive: true });
1599
+ }
1600
+ }
1601
+
1602
+ // Create ImageView, display .9 patch background
1603
+ // Use FIT_XY scaleType to ensure .9 patch correctly stretches and fills, completely consistent with system splash screen
1604
+ val imageView = ImageView(this).apply {
1605
+ layoutParams = ViewGroup.LayoutParams(
1606
+ ViewGroup.LayoutParams.MATCH_PARENT,
1607
+ ViewGroup.LayoutParams.MATCH_PARENT
1608
+ )
1609
+ // For .9 patch images, use FIT_XY to ensure complete fill, .9 patch stretch areas will work correctly
1610
+ scaleType = ImageView.ScaleType.FIT_XY
1611
+ try {
1612
+ val drawable = resources.getDrawable(
1613
+ resources.getIdentifier("${imageResourceName}", "drawable", packageName),
1614
+ null
1615
+ )
1616
+ setImageDrawable(drawable)
1617
+ } catch (e: Exception) {
1618
+ android.util.Log.e("MainActivity", "Error setting image drawable", e)
1619
+ }
1620
+ visibility = View.VISIBLE
1621
+ }
1622
+
1623
+ splashImageViewContainer?.addView(imageView)
1624
+
1625
+ // Use window.decorView to ensure on top layer (but below WebView container)
1626
+ val decorView = window.decorView as? ViewGroup
1627
+ if (decorView != null) {
1628
+ decorView.addView(splashImageViewContainer)
1629
+ // ImageView container should be below WebView container
1630
+ splashImageViewContainer?.visibility = View.VISIBLE
1631
+ splashImageViewContainer?.elevation = Float.MAX_VALUE - 1 // Below WebView container
1632
+ android.util.Log.d("MainActivity", "Splash ImageView container added to decorView")
1633
+ }
1634
+ } catch (e: Exception) {
1635
+ android.util.Log.e("MainActivity", "Error creating splash ImageView container", e)
1510
1636
  }
1511
- const activityPath = path.join(javaDir, 'SplashScreen2PrivacyPolicyActivity.kt');
1512
- const activityContent = (0, android_1.replaceTemplatePlaceholders)(android_1.ANDROID_TEMPLATES.privacyPolicyActivity, {
1513
- packageName,
1514
- });
1637
+ }
1638
+
1639
+ fun hideSplashImageViewContainer(force: Boolean = false) {
1515
1640
  try {
1516
- fs.writeFileSync(activityPath, activityContent, 'utf-8');
1517
- }
1518
- catch (error) {
1519
- console.error(`[expo-splash-screen2] Failed to generate SplashScreen2PrivacyPolicyActivity.kt:`, error);
1520
- }
1521
- }
1522
- function modifyAndroidManifestForBlendMode(manifest, packageName) {
1523
- const application = manifest.manifest.application?.[0];
1524
- const mainApplication = application && typeof application === 'object' && 'activity' in application
1525
- ? application
1526
- : null;
1527
- if (!mainApplication || !mainApplication.activity) {
1528
- return manifest;
1641
+ // If preventAutoHideImageView is true and not force hide, don't execute hide operation
1642
+ if (preventAutoHideImageView && !force) {
1643
+ android.util.Log.d("MainActivity", "hideSplashImageViewContainer prevented by preventAutoHideImageView flag")
1644
+ return
1645
+ }
1646
+
1647
+ val parent = splashImageViewContainer?.parent as? ViewGroup
1648
+ parent?.removeView(splashImageViewContainer)
1649
+
1650
+ splashImageViewContainer?.visibility = View.GONE
1651
+ splashImageViewContainer?.removeAllViews()
1652
+ splashImageViewContainer = null
1653
+ preventAutoHideImageView = false
1654
+ android.util.Log.d("MainActivity", "Splash ImageView container hidden")
1655
+ } catch (e: Exception) {
1656
+ android.util.Log.e("MainActivity", "Error hiding splash ImageView container", e)
1529
1657
  }
1530
- const mainActivityIndex = mainApplication.activity.findIndex((activity) => {
1531
- const name = activity.$?.['android:name'];
1532
- return (name === '.MainActivity' ||
1533
- name === 'MainActivity' ||
1534
- name?.endsWith('.MainActivity') ||
1535
- name === `${packageName}.MainActivity`);
1536
- });
1537
- if (mainActivityIndex === -1) {
1538
- console.warn('[expo-splash-screen2] MainActivity not found in AndroidManifest');
1539
- return manifest;
1658
+ }`;
1659
+ const classIndex = modifiedContent.indexOf(classMatch[0]) + classMatch[0].length;
1660
+ const afterClass = modifiedContent.substring(classIndex);
1661
+ const companionObjectMatch = afterClass.match(/companion\s+object\s*\{/);
1662
+ if (companionObjectMatch) {
1663
+ const companionIndex = classIndex + companionObjectMatch.index;
1664
+ modifiedContent = modifiedContent.substring(0, companionIndex) +
1665
+ imageViewCode +
1666
+ '\n' +
1667
+ modifiedContent.substring(companionIndex);
1540
1668
  }
1541
- const mainActivity = mainApplication.activity[mainActivityIndex];
1542
- if (mainActivity && mainActivity.$) {
1543
- mainActivity.$['android:theme'] = '@style/Theme.App.SplashScreen';
1669
+ else {
1670
+ const firstMethodMatch = afterClass.match(/\s+(override\s+)?fun\s+/);
1671
+ if (firstMethodMatch) {
1672
+ const insertIndex = classIndex + firstMethodMatch.index;
1673
+ modifiedContent = modifiedContent.substring(0, insertIndex) +
1674
+ imageViewCode +
1675
+ '\n' +
1676
+ modifiedContent.substring(insertIndex);
1677
+ }
1678
+ else {
1679
+ const lastBraceIndex = modifiedContent.lastIndexOf('}');
1680
+ modifiedContent = modifiedContent.substring(0, lastBraceIndex) +
1681
+ imageViewCode +
1682
+ '\n' +
1683
+ modifiedContent.substring(lastBraceIndex);
1684
+ }
1685
+ }
1686
+ const preventAutoHideMethodStart = modifiedContent.indexOf('fun preventAutoHide()');
1687
+ if (preventAutoHideMethodStart !== -1) {
1688
+ const methodSignatureEnd = modifiedContent.indexOf('{', preventAutoHideMethodStart);
1689
+ if (methodSignatureEnd !== -1) {
1690
+ let braceCount = 0;
1691
+ let methodEnd = methodSignatureEnd + 1;
1692
+ let foundStart = false;
1693
+ for (let i = methodSignatureEnd; i < modifiedContent.length; i++) {
1694
+ if (modifiedContent[i] === '{') {
1695
+ braceCount++;
1696
+ foundStart = true;
1697
+ }
1698
+ else if (modifiedContent[i] === '}') {
1699
+ braceCount--;
1700
+ if (foundStart && braceCount === 0) {
1701
+ methodEnd = i + 1;
1702
+ break;
1703
+ }
1704
+ }
1705
+ }
1706
+ const preventAutoHideCode = `
1707
+ fun preventAutoHide() {
1708
+ runOnUiThread {
1709
+ preventAutoHide = true
1710
+ preventAutoHideImageView = true
1711
+ android.util.Log.d("MainActivity", "preventAutoHide called, preventAutoHide: $preventAutoHide, preventAutoHideImageView: $preventAutoHideImageView")
1712
+ // Show ImageView container
1713
+ Handler(Looper.getMainLooper()).post {
1714
+ setupSplashImageView()
1715
+ }
1716
+ // If WebView container doesn't exist, create it
1717
+ if (webViewContainer == null) {
1718
+ android.util.Log.d("MainActivity", "WebView container is null, creating it")
1719
+ setupWebViewContainer()
1720
+ }
1544
1721
  }
1545
- const customSplashActivityIndex = mainApplication.activity.findIndex((activity) => {
1546
- const name = activity.$?.['android:name'];
1722
+ }`;
1723
+ modifiedContent = modifiedContent.substring(0, preventAutoHideMethodStart) +
1724
+ preventAutoHideCode +
1725
+ modifiedContent.substring(methodEnd);
1726
+ }
1727
+ }
1728
+ const hideWebViewMethodStart = modifiedContent.indexOf('private fun hideWebViewContainerInternal');
1729
+ if (hideWebViewMethodStart !== -1) {
1730
+ const methodSignatureEnd = modifiedContent.indexOf('{', hideWebViewMethodStart);
1731
+ if (methodSignatureEnd !== -1) {
1732
+ let braceCount = 0;
1733
+ let methodEnd = methodSignatureEnd + 1;
1734
+ let foundStart = false;
1735
+ for (let i = methodSignatureEnd; i < modifiedContent.length; i++) {
1736
+ if (modifiedContent[i] === '{') {
1737
+ braceCount++;
1738
+ foundStart = true;
1739
+ }
1740
+ else if (modifiedContent[i] === '}') {
1741
+ braceCount--;
1742
+ if (foundStart && braceCount === 0) {
1743
+ methodEnd = i + 1;
1744
+ break;
1745
+ }
1746
+ }
1747
+ }
1748
+ const hideWebViewMatch = [modifiedContent.substring(hideWebViewMethodStart, methodEnd)];
1749
+ if (hideWebViewMatch[0]) {
1750
+ const hideWebViewCode = `
1751
+ private fun hideWebViewContainerInternal(force: Boolean = false) {
1752
+ try {
1753
+ android.util.Log.d("MainActivity", "hideWebViewContainer called, force=$force, preventAutoHide=$preventAutoHide, webViewContainer=\${webViewContainer != null}")
1754
+
1755
+ // If preventAutoHide is true and not force hide, don't execute hide operation
1756
+ if (preventAutoHide && !force) {
1757
+ android.util.Log.d("MainActivity", "hideWebViewContainer prevented by preventAutoHide flag")
1758
+ return
1759
+ }
1760
+
1761
+ // Hide WebView container
1762
+ if (webViewContainer != null) {
1763
+ android.util.Log.d("MainActivity", "Hiding MainActivity WebView container")
1764
+
1765
+ // First set visibility to GONE to ensure invisible
1766
+ webViewContainer?.visibility = View.GONE
1767
+
1768
+ // Try to remove from parent view
1769
+ val parent = webViewContainer?.parent as? ViewGroup
1770
+ if (parent != null) {
1771
+ try {
1772
+ parent.removeView(webViewContainer)
1773
+ android.util.Log.d("MainActivity", "WebView container removed from parent")
1774
+ } catch (e: Exception) {
1775
+ android.util.Log.e("MainActivity", "Error removing WebView container from parent", e)
1776
+ }
1777
+ } else {
1778
+ android.util.Log.d("MainActivity", "WebView container has no parent, trying to remove from decorView")
1779
+ // If parent is null, try to remove directly from decorView
1780
+ try {
1781
+ val decorView = window.decorView as? ViewGroup
1782
+ decorView?.removeView(webViewContainer)
1783
+ android.util.Log.d("MainActivity", "WebView container removed from decorView")
1784
+ } catch (e: Exception) {
1785
+ android.util.Log.e("MainActivity", "Error removing WebView container from decorView", e)
1786
+ }
1787
+ }
1788
+
1789
+ // Clean up all child views
1790
+ webViewContainer?.removeAllViews()
1791
+
1792
+ // Clear reference
1793
+ webViewContainer = null
1794
+ android.util.Log.d("MainActivity", "MainActivity WebView container hidden and removed")
1795
+ } else {
1796
+ android.util.Log.d("MainActivity", "MainActivity WebView container is null, skipping")
1797
+ }
1798
+
1799
+ // Also hide ImageView container
1800
+ hideSplashImageViewContainer(force)
1801
+
1802
+ // Also hide SplashScreen2Activity's WebView container
1803
+ // Even if SplashScreen2Activity has finish(), instance may still exist, container may still be on window
1804
+ try {
1805
+ val customSplashActivity = SplashScreen2Activity.getInstance()
1806
+ if (customSplashActivity != null) {
1807
+ android.util.Log.d("MainActivity", "Hiding SplashScreen2Activity WebView container")
1808
+ customSplashActivity.hideWebViewContainer(force)
1809
+ } else {
1810
+ android.util.Log.d("MainActivity", "SplashScreen2Activity instance is null (already finished or not created)")
1811
+ }
1812
+ } catch (e: Exception) {
1813
+ android.util.Log.d("MainActivity", "SplashScreen2Activity not available: " + e.message)
1814
+ }
1815
+
1816
+ // Manually set MainActivity theme to @style/AppTheme after hiding splash screen
1817
+ try {
1818
+ setTheme(R.style.AppTheme)
1819
+ android.util.Log.d("MainActivity", "MainActivity theme set to @style/AppTheme")
1820
+ } catch (e: Exception) {
1821
+ android.util.Log.e("MainActivity", "Error setting theme to AppTheme", e)
1822
+ }
1823
+
1824
+ // Reset preventAutoHide flag (regardless of whether hide was successful)
1825
+ preventAutoHide = false
1826
+ android.util.Log.d("MainActivity", "preventAutoHide reset to false")
1827
+ } catch (e: Exception) {
1828
+ android.util.Log.e("MainActivity", "Error hiding WebView container", e)
1829
+ e.printStackTrace()
1830
+ // Even if error occurs, reset preventAutoHide
1831
+ preventAutoHide = false
1832
+ }
1833
+ }`;
1834
+ modifiedContent = modifiedContent.substring(0, hideWebViewMethodStart) +
1835
+ hideWebViewCode +
1836
+ modifiedContent.substring(methodEnd);
1837
+ }
1838
+ }
1839
+ }
1840
+ const onContentChangedCode = `
1841
+ override fun onContentChanged() {
1842
+ super.onContentChanged()
1843
+ // Show ImageView container and WebView container when content changed
1844
+ // ImageView container (.9图背景) should be added first, then WebView container (透明背景) on top
1845
+ Handler(Looper.getMainLooper()).post {
1846
+ setupSplashImageView()
1847
+ // Small delay to ensure ImageView container is added first
1848
+ Handler(Looper.getMainLooper()).postDelayed({
1849
+ setupWebViewContainer()
1850
+ }, 50)
1851
+ }
1852
+ }`;
1853
+ const onCreateMatchForStatusBar = modifiedContent.match(/override\s+fun\s+onCreate\s*\([^)]*\)\s*\{/);
1854
+ if (onCreateMatchForStatusBar) {
1855
+ const onCreateIndex = modifiedContent.indexOf(onCreateMatchForStatusBar[0]);
1856
+ let braceCount = 0;
1857
+ let onCreateEndIndex = onCreateIndex + onCreateMatchForStatusBar[0].length;
1858
+ let foundStart = false;
1859
+ for (let i = onCreateIndex; i < modifiedContent.length; i++) {
1860
+ if (modifiedContent[i] === '{') {
1861
+ braceCount++;
1862
+ foundStart = true;
1863
+ }
1864
+ else if (modifiedContent[i] === '}') {
1865
+ braceCount--;
1866
+ if (foundStart && braceCount === 0) {
1867
+ onCreateEndIndex = i + 1;
1868
+ break;
1869
+ }
1870
+ }
1871
+ }
1872
+ const onCreateContent = modifiedContent.substring(onCreateIndex, onCreateEndIndex);
1873
+ const isCompressed = /override\s+fun\s+onCreate\s*\([^)]*\)\s*\{[^}]*super\.onCreate[^}]*window\.statusBarColor[^}]*window\.navigationBarColor[^}]*\}/.test(onCreateContent) &&
1874
+ !onCreateContent.includes('\n super.onCreate') &&
1875
+ !onCreateContent.includes('\n window.statusBarColor');
1876
+ if (isCompressed) {
1877
+ const superOnCreateMatch = onCreateContent.match(/super\.onCreate\([^)]*\)/);
1878
+ const statusBarMatch = onCreateContent.match(/window\.statusBarColor\s*=\s*[^}\n]+/);
1879
+ const navBarMatch = onCreateContent.match(/window\.navigationBarColor\s*=\s*[^}\n]+/);
1880
+ if (superOnCreateMatch && statusBarMatch && navBarMatch) {
1881
+ const formattedOnCreate = `
1882
+ override fun onCreate(savedInstanceState: Bundle?) {
1883
+ ${superOnCreateMatch[0]}
1884
+ ${statusBarMatch[0].trim()}
1885
+ ${navBarMatch[0].replace(/[}]/g, '').trim()}
1886
+ }`;
1887
+ modifiedContent = modifiedContent.substring(0, onCreateIndex) +
1888
+ formattedOnCreate +
1889
+ modifiedContent.substring(onCreateEndIndex);
1890
+ }
1891
+ }
1892
+ else {
1893
+ const hasStatusBarColor = onCreateContent.includes('window.statusBarColor');
1894
+ const hasNavigationBarColor = onCreateContent.includes('window.navigationBarColor');
1895
+ if (!hasStatusBarColor || !hasNavigationBarColor) {
1896
+ const superOnCreateMatch = onCreateContent.match(/super\.onCreate\([^)]*\)/);
1897
+ if (superOnCreateMatch) {
1898
+ const superOnCreateEndIndex = onCreateContent.indexOf(superOnCreateMatch[0]) + superOnCreateMatch[0].length;
1899
+ const nextLineIndex = onCreateContent.indexOf('\n', superOnCreateEndIndex);
1900
+ const insertPos = onCreateIndex + (nextLineIndex !== -1 ? nextLineIndex + 1 : superOnCreateEndIndex);
1901
+ const statusBarCode = `
1902
+ window.statusBarColor = android.graphics.Color.TRANSPARENT
1903
+ window.navigationBarColor = android.graphics.Color.TRANSPARENT`;
1904
+ modifiedContent = modifiedContent.substring(0, insertPos) +
1905
+ statusBarCode + '\n' +
1906
+ modifiedContent.substring(insertPos);
1907
+ }
1908
+ }
1909
+ }
1910
+ }
1911
+ if (!modifiedContent.includes('override fun onContentChanged()')) {
1912
+ const onCreateMatchForInsert = modifiedContent.match(/override\s+fun\s+onCreate\s*\([^)]*\)\s*\{/);
1913
+ if (onCreateMatchForInsert) {
1914
+ const onCreateIndexForInsert = modifiedContent.indexOf(onCreateMatchForInsert[0]);
1915
+ let braceCount = 0;
1916
+ let onCreateEndIndexForInsert = onCreateIndexForInsert + onCreateMatchForInsert[0].length;
1917
+ let foundStart = false;
1918
+ for (let i = onCreateIndexForInsert; i < modifiedContent.length; i++) {
1919
+ if (modifiedContent[i] === '{') {
1920
+ braceCount++;
1921
+ foundStart = true;
1922
+ }
1923
+ else if (modifiedContent[i] === '}') {
1924
+ braceCount--;
1925
+ if (foundStart && braceCount === 0) {
1926
+ onCreateEndIndexForInsert = i + 1;
1927
+ break;
1928
+ }
1929
+ }
1930
+ }
1931
+ modifiedContent = modifiedContent.substring(0, onCreateEndIndexForInsert) +
1932
+ '\n' + onContentChangedCode + '\n' +
1933
+ modifiedContent.substring(onCreateEndIndexForInsert);
1934
+ }
1935
+ else {
1936
+ const getMainComponentMatch = modifiedContent.match(/override\s+fun\s+getMainComponentName/);
1937
+ if (getMainComponentMatch) {
1938
+ const insertPos = modifiedContent.indexOf(getMainComponentMatch[0]);
1939
+ modifiedContent = modifiedContent.substring(0, insertPos) +
1940
+ onContentChangedCode + '\n\n' +
1941
+ modifiedContent.substring(insertPos);
1942
+ }
1943
+ }
1944
+ }
1945
+ return modifiedContent;
1946
+ }
1947
+ function generatePrivacyPolicyActivity(packageName, projectRoot, androidMainPath) {
1948
+ const javaDir = path.join(androidMainPath, 'java', ...packageName.split('.'));
1949
+ if (!fs.existsSync(javaDir)) {
1950
+ fs.mkdirSync(javaDir, { recursive: true });
1951
+ }
1952
+ const activityPath = path.join(javaDir, 'SplashScreen2PrivacyPolicyActivity.kt');
1953
+ const activityContent = (0, android_1.replaceTemplatePlaceholders)(android_1.ANDROID_TEMPLATES.privacyPolicyActivity, {
1954
+ packageName,
1955
+ });
1956
+ try {
1957
+ fs.writeFileSync(activityPath, activityContent, 'utf-8');
1958
+ }
1959
+ catch (error) {
1960
+ console.error(`[expo-splash-screen2] Failed to generate SplashScreen2PrivacyPolicyActivity.kt:`, error);
1961
+ }
1962
+ }
1963
+ function modifyAndroidManifestForImageMode(manifest, packageName) {
1964
+ const application = manifest.manifest.application?.[0];
1965
+ const mainApplication = application && typeof application === 'object' && 'activity' in application
1966
+ ? application
1967
+ : null;
1968
+ if (!mainApplication || !mainApplication.activity) {
1969
+ return manifest;
1970
+ }
1971
+ const mainActivityIndex = mainApplication.activity.findIndex((activity) => {
1972
+ const name = activity.$?.['android:name'];
1973
+ return (name === '.MainActivity' ||
1974
+ name === 'MainActivity' ||
1975
+ name?.endsWith('.MainActivity') ||
1976
+ name === `${packageName}.MainActivity`);
1977
+ });
1978
+ if (mainActivityIndex === -1) {
1979
+ console.warn('[expo-splash-screen2] MainActivity not found in AndroidManifest');
1980
+ return manifest;
1981
+ }
1982
+ const mainActivity = mainApplication.activity[mainActivityIndex];
1983
+ if (mainActivity && mainActivity.$) {
1984
+ mainActivity.$['android:theme'] = '@style/AppTheme';
1985
+ }
1986
+ return manifest;
1987
+ }
1988
+ function modifyAndroidManifestForBlendMode(manifest, packageName) {
1989
+ const application = manifest.manifest.application?.[0];
1990
+ const mainApplication = application && typeof application === 'object' && 'activity' in application
1991
+ ? application
1992
+ : null;
1993
+ if (!mainApplication || !mainApplication.activity) {
1994
+ return manifest;
1995
+ }
1996
+ const mainActivityIndex = mainApplication.activity.findIndex((activity) => {
1997
+ const name = activity.$?.['android:name'];
1998
+ return (name === '.MainActivity' ||
1999
+ name === 'MainActivity' ||
2000
+ name?.endsWith('.MainActivity') ||
2001
+ name === `${packageName}.MainActivity`);
2002
+ });
2003
+ if (mainActivityIndex === -1) {
2004
+ console.warn('[expo-splash-screen2] MainActivity not found in AndroidManifest');
2005
+ return manifest;
2006
+ }
2007
+ const mainActivity = mainApplication.activity[mainActivityIndex];
2008
+ if (mainActivity && mainActivity.$) {
2009
+ mainActivity.$['android:theme'] = '@style/Theme.App.SplashScreen';
2010
+ }
2011
+ const customSplashActivityIndex = mainApplication.activity.findIndex((activity) => {
2012
+ const name = activity.$?.['android:name'];
1547
2013
  return (name === `.SplashScreen2Activity` ||
1548
2014
  name === 'SplashScreen2Activity' ||
1549
2015
  name?.endsWith(`.SplashScreen2Activity`) ||
@@ -1829,1295 +2295,155 @@ function copyHtmlFileForIOS(projectRoot, iosPath, localHtmlPath) {
1829
2295
  }
1830
2296
  let targetDir = path.join(iosPath, 'MyNewExpoSplashDemo');
1831
2297
  try {
1832
- const entries = fs.readdirSync(iosPath, { withFileTypes: true });
1833
- const projectDir = entries
1834
- .filter((e) => e.isDirectory())
1835
- .map((e) => e.name)
1836
- .find((d) => fs.existsSync(path.join(iosPath, `${d}.xcodeproj`)));
1837
- if (projectDir) {
1838
- targetDir = path.join(iosPath, projectDir);
1839
- }
1840
- }
1841
- catch { }
1842
- console.log(`[expo-splash-screen2] [iOS] targetDir: ${targetDir}`);
1843
- if (!fs.existsSync(targetDir)) {
1844
- fs.mkdirSync(targetDir, { recursive: true });
1845
- }
1846
- const htmlContent = fs.readFileSync(sourcePath, 'utf-8');
1847
- const htmlDir = path.dirname(sourcePath);
1848
- console.log(`[expo-splash-screen2] [iOS] htmlDir: ${htmlDir}`);
1849
- const imagePaths = extractImagePaths(htmlContent, htmlDir);
1850
- console.log(`[expo-splash-screen2] [iOS] extractImagePaths found: ${imagePaths.length} images`);
1851
- imagePaths.forEach(({ original, absolute }) => {
1852
- console.log(`[expo-splash-screen2] [iOS] - original: ${original}, absolute: ${absolute}`);
1853
- });
1854
- const assetsDir = path.join(htmlDir, 'assets');
1855
- console.log(`[expo-splash-screen2] [iOS] checking assetsDir: ${assetsDir}`);
1856
- console.log(`[expo-splash-screen2] [iOS] assetsDir exists: ${fs.existsSync(assetsDir)}`);
1857
- if (fs.existsSync(assetsDir) && fs.statSync(assetsDir).isDirectory()) {
1858
- const allFiles = fs.readdirSync(assetsDir);
1859
- console.log(`[expo-splash-screen2] [iOS] assetsDir all files: ${allFiles.join(', ')}`);
1860
- const imageFiles = allFiles.filter(f => /\.(png|jpg|jpeg|gif|svg|webp|ico)$/i.test(f));
1861
- console.log(`[expo-splash-screen2] [iOS] assetsDir image files: ${imageFiles.join(', ')}`);
1862
- imageFiles.forEach(imgFile => {
1863
- const srcPath = path.join(assetsDir, imgFile);
1864
- const absolutePath = srcPath;
1865
- imagePaths.push({ original: `./assets/${imgFile}`, absolute: absolutePath });
1866
- console.log(`[expo-splash-screen2] [iOS] added from assets: ./assets/${imgFile} -> ${absolutePath}`);
1867
- });
1868
- }
1869
- const imagesDir = path.join(htmlDir, 'images');
1870
- console.log(`[expo-splash-screen2] [iOS] checking imagesDir: ${imagesDir}`);
1871
- console.log(`[expo-splash-screen2] [iOS] imagesDir exists: ${fs.existsSync(imagesDir)}`);
1872
- if (fs.existsSync(imagesDir) && fs.statSync(imagesDir).isDirectory()) {
1873
- const imageFiles = fs.readdirSync(imagesDir).filter(f => /\.(png|jpg|jpeg|gif|svg|webp|ico)$/i.test(f));
1874
- console.log(`[expo-splash-screen2] [iOS] imagesDir image files: ${imageFiles.join(', ')}`);
1875
- imageFiles.forEach(imgFile => {
1876
- const srcPath = path.join(imagesDir, imgFile);
1877
- const absolutePath = srcPath;
1878
- imagePaths.push({ original: `./images/${imgFile}`, absolute: absolutePath });
1879
- });
1880
- }
1881
- console.log(`[expo-splash-screen2] [iOS] total imagePaths after scanning: ${imagePaths.length}`);
1882
- const imagePathMap = new Map();
1883
- imagePaths.forEach(({ original, absolute }) => {
1884
- const fileNameWithHash = path.basename(absolute);
1885
- const fileNameWithoutHash = removeHashFromFileName(fileNameWithHash);
1886
- const newPath = `./${fileNameWithoutHash}`;
1887
- console.log(`[expo-splash-screen2] [iOS] processing: ${original} -> ${newPath} (file: ${absolute})`);
1888
- const targetImagePath = path.join(targetDir, fileNameWithoutHash);
1889
- console.log(`[expo-splash-screen2] [iOS] copying to: ${targetImagePath}`);
1890
- if (fs.existsSync(absolute)) {
1891
- fs.copyFileSync(absolute, targetImagePath);
1892
- console.log(`[expo-splash-screen2] [iOS] copied successfully: ${fileNameWithoutHash}`);
1893
- imagePathMap.set(original, newPath);
1894
- const normalizedOriginal = original.startsWith('./') ? original : `./${original}`;
1895
- imagePathMap.set(normalizedOriginal, newPath);
1896
- if (original.startsWith('./')) {
1897
- imagePathMap.set(original.substring(2), newPath);
1898
- }
1899
- if (original.startsWith('./images/')) {
1900
- imagePathMap.set(original, newPath);
1901
- imagePathMap.set(original.substring(2), newPath);
1902
- imagePathMap.set(original.substring(10), newPath);
1903
- }
1904
- if (original.startsWith('./assets/')) {
1905
- imagePathMap.set(original, newPath);
1906
- imagePathMap.set(original.substring(2), newPath);
1907
- const originalFileName = path.basename(original);
1908
- if (originalFileName !== fileNameWithoutHash) {
1909
- imagePathMap.set(original.replace(originalFileName, fileNameWithoutHash), newPath);
1910
- }
1911
- }
1912
- }
1913
- });
1914
- let updatedHtmlContent = htmlContent;
1915
- updatedHtmlContent = updatedHtmlContent.replace(/<img([^>]+)src\s*=\s*["']([^"']+)["']/gi, (match, attrs, srcPath) => {
1916
- if (srcPath.startsWith('http') || srcPath.startsWith('data:')) {
1917
- return match;
1918
- }
1919
- const newPath = imagePathMap.get(srcPath) || imagePathMap.get(`./${srcPath}`) || imagePathMap.get(srcPath.replace(/^\.\//, ''));
1920
- if (newPath) {
1921
- return `<img${attrs}src="${newPath}"`;
1922
- }
1923
- return match;
1924
- });
1925
- updatedHtmlContent = updatedHtmlContent.replace(/url\s*\(\s*["']?([^"')]+)["']?\s*\)/gi, (match, urlPath) => {
1926
- if (urlPath.startsWith('http') || urlPath.startsWith('data:')) {
1927
- return match;
1928
- }
1929
- const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico'];
1930
- const lowerPath = urlPath.toLowerCase();
1931
- if (!imageExtensions.some(ext => lowerPath.includes(ext))) {
1932
- return match;
1933
- }
1934
- const newPath = imagePathMap.get(urlPath) || imagePathMap.get(`./${urlPath}`) || imagePathMap.get(urlPath.replace(/^\.\//, ''));
1935
- if (newPath) {
1936
- return `url("${newPath}")`;
1937
- }
1938
- return match;
1939
- });
1940
- updatedHtmlContent = updatedHtmlContent.replace(/(["'])(\.\/images\/[^"']+\.(png|jpg|jpeg|gif|svg|webp|ico))(["'])/gi, (match, quote1, imgPath, ext, quote2) => {
1941
- const fileName = path.basename(imgPath);
1942
- const fileNameWithoutHash = removeHashFromFileName(fileName);
1943
- const newPath = imagePathMap.get(`./images/${fileName}`) ||
1944
- imagePathMap.get(`./images/${fileNameWithoutHash}`) ||
1945
- imagePathMap.get(`./${fileName}`) ||
1946
- imagePathMap.get(`./${fileNameWithoutHash}`) ||
1947
- `./${fileNameWithoutHash}`;
1948
- return `${quote1}${newPath}${quote2}`;
1949
- });
1950
- updatedHtmlContent = updatedHtmlContent.replace(/(["'])(\.\/assets\/[^"']+\.(png|jpg|jpeg|gif|svg|webp|ico))(["'])/gi, (match, quote1, imgPath, ext, quote2) => {
1951
- const fileName = path.basename(imgPath);
1952
- const fileNameWithoutHash = removeHashFromFileName(fileName);
1953
- const newPath = imagePathMap.get(imgPath) ||
1954
- imagePathMap.get(`./assets/${imgPath.substring(2)}`) ||
1955
- `./${fileNameWithoutHash}`;
1956
- return `${quote1}${newPath}${quote2}`;
1957
- });
1958
- updatedHtmlContent = updatedHtmlContent.replace(/(\.\/images\/[^\s"'`;,\)]+\.(png|jpg|jpeg|gif|svg|webp|ico))/gi, (match, imgPath) => {
1959
- const fileName = path.basename(imgPath);
1960
- const fileNameWithoutHash = removeHashFromFileName(fileName);
1961
- const newPath = imagePathMap.get(`./images/${fileName}`) ||
1962
- imagePathMap.get(`./images/${fileNameWithoutHash}`) ||
1963
- imagePathMap.get(`./${fileName}`) ||
1964
- imagePathMap.get(`./${fileNameWithoutHash}`) ||
1965
- `./${fileNameWithoutHash}`;
1966
- return newPath;
1967
- });
1968
- updatedHtmlContent = updatedHtmlContent.replace(/(\.\/assets\/[^\s"'`;,\)]+\.(png|jpg|jpeg|gif|svg|webp|ico))/gi, (match, imgPath) => {
1969
- const fileName = path.basename(imgPath);
1970
- const fileNameWithoutHash = removeHashFromFileName(fileName);
1971
- const newPath = imagePathMap.get(imgPath) ||
1972
- imagePathMap.get(`./assets/${imgPath.substring(2)}`) ||
1973
- `./${fileNameWithoutHash}`;
1974
- return newPath;
1975
- });
1976
- const targetPath = path.join(targetDir, 'index.html');
1977
- fs.writeFileSync(targetPath, updatedHtmlContent, 'utf-8');
1978
- }
1979
- catch (error) {
1980
- console.error(`[expo-splash-screen2] Error copying HTML file for iOS: ${error}`);
1981
- }
1982
- }
1983
- function generateSplashScreen2Service(bundleIdentifier, projectRoot, iosPath, projectName) {
1984
- const targetDir = path.join(iosPath, projectName);
1985
- if (!fs.existsSync(targetDir)) {
1986
- fs.mkdirSync(targetDir, { recursive: true });
1987
- }
1988
- const servicePath = path.join(targetDir, 'SplashScreen2Service.swift');
1989
- const serviceContent = `import UIKit
1990
- import WebKit
1991
-
1992
- // Protocol definition, used to replace AppDelegate type
1993
- @objc public protocol AppDelegateProtocol {
1994
- @objc func startReactNativeIfNeeded()
1995
- }
1996
-
1997
- // Similar to EXSplashScreenService, manages splash screen display and hiding
1998
- public class SplashScreen2Service: NSObject {
1999
- private var splashScreenControllers: [UIViewController: SplashScreen2ViewController] = [:]
2000
- private weak var observingRootViewController: UIViewController?
2001
- // Global preventAutoHide state, applied to newly created splash screens
2002
- private var globalPreventAutoHide: Bool = false
2003
- private static let sharedInstance = SplashScreen2Service()
2004
-
2005
- public static var shared: SplashScreen2Service {
2006
- return sharedInstance
2007
- }
2008
-
2009
- private override init() {
2010
- super.init()
2011
- }
2012
-
2013
- // Show splash screen (similar to EXSplashScreenService.showSplashScreenFor)
2014
- public func showSplashScreenFor(_ viewController: UIViewController) {
2015
- print("[SplashScreen2Service] showSplashScreenFor called for viewController: \\(viewController)")
2016
- print("[SplashScreen2Service] showSplashScreenFor - globalPreventAutoHide: \\(globalPreventAutoHide)")
2017
-
2018
- // If already exists, clean up old one first
2019
- // Note: Using force=true here because we want to replace the old splash screen
2020
- // But if globalPreventAutoHide=true, we should keep the old one instead of cleaning it up
2021
- if let existingController = splashScreenControllers[viewController] {
2022
- if globalPreventAutoHide {
2023
- print("[SplashScreen2Service] showSplashScreenFor - globalPreventAutoHide is true, keeping existing splash screen")
2024
- // If preventAutoHide is already set, no need to recreate
2025
- // Ensure splash screen is on top layer and visible
2026
- if let splashVC = existingController.splashViewControllerInstance {
2027
- splashVC.view.isHidden = false
2028
- splashVC.view.alpha = 1.0
2029
- viewController.view.bringSubviewToFront(splashVC.view)
2030
- print("[SplashScreen2Service] showSplashScreenFor - Brought existing splash screen to front and ensured visibility")
2031
- }
2032
- return
2033
- } else {
2034
- print("[SplashScreen2Service] Splash screen already exists for view controller, cleaning up old one")
2035
- existingController.hide(force: true)
2036
- splashScreenControllers.removeValue(forKey: viewController)
2037
- }
2038
- }
2039
-
2040
- // Create SplashScreen2ViewController instance
2041
- let splashVC = SplashScreen2ViewController()
2042
- let splashScreenController = SplashScreen2ViewController(splashViewController: splashVC)
2043
-
2044
- // If global preventAutoHide state is true, apply immediately
2045
- if globalPreventAutoHide {
2046
- print("[SplashScreen2Service] showSplashScreenFor - Applying global preventAutoHide state")
2047
- splashScreenController.preventAutoHide()
2048
- // Ensure splash screen is visible (set before adding to parent view)
2049
- splashVC.view.isHidden = false
2050
- splashVC.view.alpha = 1.0
2051
- }
2052
-
2053
- // Set view's frame first, ensure correct size
2054
- // This must be done before adding to parent view
2055
- splashVC.view.frame = viewController.view.bounds
2056
-
2057
- // Print size information for debugging
2058
- print("[SplashScreen2Service] showSplashScreenFor - viewController.view.frame: \\(viewController.view.frame)")
2059
- print("[SplashScreen2Service] showSplashScreenFor - viewController.view.bounds: \\(viewController.view.bounds)")
2060
- print("[SplashScreen2Service] showSplashScreenFor - UIScreen.main.bounds: \\(UIScreen.main.bounds)")
2061
- print("[SplashScreen2Service] showSplashScreenFor - splashVC.view.frame (before addSubview): \\(splashVC.view.frame)")
2062
-
2063
- // Add SplashScreen2ViewController as child view controller (maintain lifecycle)
2064
- // This must be called before addSubview to ensure viewDidLoad is called at the right time
2065
- viewController.addChild(splashVC)
2066
-
2067
- // Add SplashScreen2ViewController's view to target view controller's view
2068
- viewController.view.addSubview(splashVC.view)
2069
- splashVC.view.translatesAutoresizingMaskIntoConstraints = false
2070
-
2071
- // Set constraints to ensure full screen display
2072
- NSLayoutConstraint.activate([
2073
- splashVC.view.topAnchor.constraint(equalTo: viewController.view.topAnchor),
2074
- splashVC.view.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor),
2075
- splashVC.view.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor),
2076
- splashVC.view.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor)
2077
- ])
2078
-
2079
- // 确保在最上层
2080
- viewController.view.bringSubviewToFront(splashVC.view)
2081
-
2082
- // 完成子 view controller 的添加
2083
- splashVC.didMove(toParent: viewController)
2084
-
2085
- // 强制布局更新,确保约束生效
2086
- viewController.view.setNeedsLayout()
2087
- viewController.view.layoutIfNeeded()
2088
- splashVC.view.setNeedsLayout()
2089
- splashVC.view.layoutIfNeeded()
2090
-
2091
- // 打印约束后的尺寸
2092
- print("[SplashScreen2Service] showSplashScreenFor - After constraints, splashVC.view.frame: \\(splashVC.view.frame)")
2093
- print("[SplashScreen2Service] showSplashScreenFor - After constraints, splashVC.view.bounds: \\(splashVC.view.bounds)")
2094
- print("[SplashScreen2Service] showSplashScreenFor - splashVC.view.superview: \\(String(describing: splashVC.view.superview))")
2095
- print("[SplashScreen2Service] showSplashScreenFor - splashVC.view.window: \\(String(describing: splashVC.view.window))")
2096
-
2097
- // 确保 WebView 已经正确挂载
2098
- // 延迟一点时间,确保 viewDidLoad 和 setupWebView 已经完成
2099
- DispatchQueue.main.async {
2100
- print("[SplashScreen2Service] showSplashScreenFor - After async, splashVC.view.frame: \\(splashVC.view.frame)")
2101
- print("[SplashScreen2Service] showSplashScreenFor - After async, splashVC.view.subviews.count: \\(splashVC.view.subviews.count)")
2102
- print("[SplashScreen2Service] showSplashScreenFor - After async, splashVC.view.isHidden: \\(splashVC.view.isHidden)")
2103
- print("[SplashScreen2Service] showSplashScreenFor - After async, splashVC.view.alpha: \\(splashVC.view.alpha)")
2104
- print("[SplashScreen2Service] showSplashScreenFor - After async, splashVC.view.superview: \\(String(describing: splashVC.view.superview))")
2105
- print("[SplashScreen2Service] showSplashScreenFor - After async, splashVC.view.window: \\(String(describing: splashVC.view.window))")
2106
-
2107
- // 检查是否有其他视图遮挡
2108
- if let superview = splashVC.view.superview {
2109
- print("[SplashScreen2Service] showSplashScreenFor - superview.subviews.count: \\(superview.subviews.count)")
2110
- for (index, subview) in superview.subviews.enumerated() {
2111
- print("[SplashScreen2Service] showSplashScreenFor - superview.subview[\\(index)]: \\(type(of: subview)), frame: \\(subview.frame), isHidden: \\(subview.isHidden), alpha: \\(subview.alpha)")
2112
- }
2113
- }
2114
-
2115
- for (index, subview) in splashVC.view.subviews.enumerated() {
2116
- print("[SplashScreen2Service] showSplashScreenFor - subview[\\(index)]: \\(type(of: subview)), frame: \\(subview.frame), isHidden: \\(subview.isHidden), alpha: \\(subview.alpha)")
2117
-
2118
- // 确保 WebView 可见
2119
- if let webView = subview as? WKWebView {
2120
- webView.isHidden = false
2121
- webView.alpha = 1.0
2122
- print("[SplashScreen2Service] showSplashScreenFor - WebView visibility set: isHidden=\\(webView.isHidden), alpha=\\(webView.alpha)")
2123
- }
2124
- }
2125
-
2126
- // 确保 view 在最上层
2127
- if let superview = splashVC.view.superview {
2128
- superview.bringSubviewToFront(splashVC.view)
2129
- print("[SplashScreen2Service] showSplashScreenFor - Brought splashVC.view to front")
2130
- }
2131
- }
2132
-
2133
- splashScreenControllers[viewController] = splashScreenController
2134
- splashScreenController.show()
2135
- }
2136
-
2137
- // 隐藏 splash screen(类似 EXSplashScreenService.hideSplashScreenFor)
2138
- public func hideSplashScreenFor(_ viewController: UIViewController, force: Bool = false) {
2139
- print("[SplashScreen2Service] hideSplashScreenFor called for viewController: \\(viewController), force: \\(force)")
2140
- print("[SplashScreen2Service] hideSplashScreenFor - globalPreventAutoHide: \\(globalPreventAutoHide)")
2141
-
2142
- guard let controller = splashScreenControllers[viewController] else {
2143
- print("[SplashScreen2Service] No splash screen found for view controller")
2144
- return
2145
- }
2146
-
2147
- // 如果 globalPreventAutoHide 为 true 且 force 为 false,不执行隐藏操作
2148
- if globalPreventAutoHide && !force {
2149
- print("[SplashScreen2Service] hideSplashScreenFor - globalPreventAutoHide is true and force is false, ignoring hide call")
2150
- print("[SplashScreen2Service] hideSplashScreenFor - Stack trace: \\(Thread.callStackSymbols.prefix(5).joined(separator: "\\n"))")
2151
- // 确保 splash screen 仍然可见且在最上层
2152
- if let splashVC = controller.splashViewControllerInstance {
2153
- splashVC.view.isHidden = false
2154
- splashVC.view.alpha = 1.0
2155
- if let parent = splashVC.parent {
2156
- parent.view.bringSubviewToFront(splashVC.view)
2157
- } else if let superview = splashVC.view.superview {
2158
- superview.bringSubviewToFront(splashVC.view)
2159
- }
2160
- print("[SplashScreen2Service] hideSplashScreenFor - Ensured splash screen is still visible and on top")
2161
- }
2162
- return
2163
- }
2164
-
2165
- print("[SplashScreen2Service] hideSplashScreenFor - Proceeding with hide, force: \\(force)")
2166
- // 使用 force=true 强制隐藏,即使 preventAutoHide 被调用
2167
- controller.hide(force: force)
2168
- splashScreenControllers.removeValue(forKey: viewController)
2169
- }
2170
-
2171
- // 隐藏所有 splash screen(用于强制隐藏所有已知的 splash screen)
2172
- public func hideAllSplashScreens(force: Bool = true) {
2173
- print("[SplashScreen2Service] hideAllSplashScreens called, force: \\(force)")
2174
- print("[SplashScreen2Service] hideAllSplashScreens - splashScreenControllers count: \\(splashScreenControllers.count)")
2175
-
2176
- // 复制字典的键,因为我们在迭代过程中会修改字典
2177
- let allViewControllers = Array(splashScreenControllers.keys)
2178
-
2179
- for viewController in allViewControllers {
2180
- print("[SplashScreen2Service] hideAllSplashScreens - Hiding splash screen for: \\(viewController)")
2181
- hideSplashScreenFor(viewController, force: force)
2182
- }
2183
-
2184
- print("[SplashScreen2Service] hideAllSplashScreens - Completed, remaining count: \\(splashScreenControllers.count)")
2185
- }
2186
-
2187
- // 防止自动隐藏(类似 EXSplashScreenService.preventSplashScreenAutoHideFor)
2188
- public func preventAutoHideFor(_ viewController: UIViewController) {
2189
- print("[SplashScreen2Service] preventAutoHideFor called for viewController: \\(viewController)")
2190
- print("[SplashScreen2Service] preventAutoHideFor - Stack trace: \\(Thread.callStackSymbols.prefix(5).joined(separator: "\\n"))")
2191
-
2192
- // 设置全局 preventAutoHide 状态(必须在最开始设置)
2193
- globalPreventAutoHide = true
2194
- print("[SplashScreen2Service] preventAutoHideFor - Set globalPreventAutoHide to true")
2195
-
2196
- // 如果还没有 splash screen,先创建一个
2197
- if splashScreenControllers[viewController] == nil {
2198
- print("[SplashScreen2Service] preventAutoHideFor - No splash screen found, creating one first")
2199
- showSplashScreenFor(viewController)
2200
- }
2201
-
2202
- // 对所有现有的 splash screen 应用 preventAutoHide
2203
- for (vc, controller) in splashScreenControllers {
2204
- print("[SplashScreen2Service] preventAutoHideFor - Applying preventAutoHide to existing splash screen for viewController: \\(vc)")
2205
- controller.preventAutoHide()
2206
- // 确保 splash screen 可见且在最上层
2207
- if let splashVC = controller.splashViewControllerInstance {
2208
- splashVC.view.isHidden = false
2209
- splashVC.view.alpha = 1.0
2210
- if let parent = splashVC.parent {
2211
- parent.view.bringSubviewToFront(splashVC.view)
2212
- } else if let superview = splashVC.view.superview {
2213
- superview.bringSubviewToFront(splashVC.view)
2214
- }
2215
- print("[SplashScreen2Service] preventAutoHideFor - Ensured splash screen is visible and on top for viewController: \\(vc)")
2216
- }
2217
- }
2218
-
2219
- guard let controller = splashScreenControllers[viewController] else {
2220
- print("[SplashScreen2Service] preventAutoHideFor - Failed to create or find splash screen")
2221
- return
2222
- }
2223
-
2224
- print("[SplashScreen2Service] preventAutoHideFor - Calling preventAutoHide on controller")
2225
- controller.preventAutoHide()
2226
-
2227
- // 确保 splash screen 可见且在最上层
2228
- if let splashVC = controller.splashViewControllerInstance {
2229
- splashVC.view.isHidden = false
2230
- splashVC.view.alpha = 1.0
2231
- if let parent = splashVC.parent {
2232
- parent.view.bringSubviewToFront(splashVC.view)
2233
- } else if let superview = splashVC.view.superview {
2234
- superview.bringSubviewToFront(splashVC.view)
2235
- }
2236
- print("[SplashScreen2Service] preventAutoHideFor - Ensured splash screen is visible and on top")
2237
- }
2238
-
2239
- print("[SplashScreen2Service] preventAutoHideFor - preventAutoHide called successfully")
2240
- }
2241
-
2242
- // 添加 rootViewController 监听(类似 EXSplashScreenService.addRootViewControllerListener)
2243
- public func addRootViewControllerListener() {
2244
- guard Thread.isMainThread else {
2245
- DispatchQueue.main.async { [weak self] in
2246
- self?.addRootViewControllerListener()
2247
- }
2248
- return
2249
- }
2250
-
2251
- // 如果已经有监听器,先移除旧的
2252
- if observingRootViewController != nil {
2253
- print("[SplashScreen2Service] addRootViewControllerListener: Already observing, removing old listener first")
2254
- removeRootViewControllerListener()
2255
- }
2256
-
2257
- if let window = UIApplication.shared.keyWindow {
2258
- window.addObserver(self, forKeyPath: "rootViewController", options: .new, context: nil)
2259
-
2260
- // 如果已经有 rootViewController,立即显示 splash screen
2261
- if let rootViewController = window.rootViewController {
2262
- print("[SplashScreen2Service] addRootViewControllerListener: Found existing rootViewController: \\(rootViewController)")
2263
- print("[SplashScreen2Service] addRootViewControllerListener - globalPreventAutoHide: \\(globalPreventAutoHide)")
2264
-
2265
- // 只有当 rootViewController 不是当前观察的对象时才添加监听器
2266
- if rootViewController != observingRootViewController {
2267
- rootViewController.addObserver(self, forKeyPath: "view", options: .new, context: nil)
2268
- observingRootViewController = rootViewController
2269
-
2270
- // 立即显示 splash screen(只有当还没有显示时才显示)
2271
- // 如果 globalPreventAutoHide 为 true,且已经存在 splash screen,不需要重新创建
2272
- if splashScreenControllers[rootViewController] == nil {
2273
- if globalPreventAutoHide {
2274
- print("[SplashScreen2Service] addRootViewControllerListener - globalPreventAutoHide is true but no splash screen found, this should not happen")
2275
- }
2276
- showSplashScreenFor(rootViewController)
2277
- } else {
2278
- print("[SplashScreen2Service] addRootViewControllerListener: Splash screen already exists for rootViewController, skipping")
2279
- // 如果 globalPreventAutoHide 为 true,确保 splash screen 在最上层
2280
- if globalPreventAutoHide, let controller = splashScreenControllers[rootViewController] {
2281
- print("[SplashScreen2Service] addRootViewControllerListener - Ensuring splash screen is on top")
2282
- if let splashVC = controller.splashViewControllerInstance {
2283
- rootViewController.view.bringSubviewToFront(splashVC.view)
2284
- }
2285
- }
2286
- }
2287
- }
2288
- } else {
2289
- // 如果没有 rootViewController,创建一个临时的 view controller 来显示 splash screen
2290
- // 这确保在 RN 启动之前就能看到 splash screen
2291
- print("[SplashScreen2Service] addRootViewControllerListener: No rootViewController, creating temp one")
2292
- let tempViewController = UIViewController()
2293
- tempViewController.view.backgroundColor = .clear
2294
- window.rootViewController = tempViewController
2295
- window.makeKeyAndVisible()
2296
-
2297
- tempViewController.addObserver(self, forKeyPath: "view", options: .new, context: nil)
2298
- observingRootViewController = tempViewController
2299
-
2300
- // 立即显示 splash screen
2301
- showSplashScreenFor(tempViewController)
2302
- }
2303
- } else {
2304
- print("[SplashScreen2Service] addRootViewControllerListener: No keyWindow found")
2305
- }
2306
- }
2307
-
2308
- // 移除 rootViewController 监听(类似 EXSplashScreenService.removeRootViewControllerListener)
2309
- public func removeRootViewControllerListener() {
2310
- guard Thread.isMainThread else {
2311
- DispatchQueue.main.async { [weak self] in
2312
- self?.removeRootViewControllerListener()
2313
- }
2314
- return
2315
- }
2316
-
2317
- if let rootViewController = observingRootViewController {
2318
- if let window = rootViewController.view.window {
2319
- window.removeObserver(self, forKeyPath: "rootViewController")
2320
- }
2321
- rootViewController.removeObserver(self, forKeyPath: "view")
2322
- observingRootViewController = nil
2323
- }
2324
- }
2325
-
2326
- // KVO 监听(类似 EXSplashScreenService.observeValueForKeyPath)
2327
- public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
2328
- if let window = object as? UIWindow, keyPath == "rootViewController" {
2329
- if let newRootViewController = change?[.newKey] as? UIViewController,
2330
- newRootViewController != observingRootViewController {
2331
- print("[SplashScreen2Service] rootViewController changed from \\(String(describing: observingRootViewController)) to \\(newRootViewController)")
2332
- print("[SplashScreen2Service] rootViewController changed - globalPreventAutoHide: \\(globalPreventAutoHide)")
2333
-
2334
- // 尝试复用已有的 splash screen(无论是否调用 preventAutoHide)
2335
- if let oldRootViewController = observingRootViewController,
2336
- let oldController = splashScreenControllers[oldRootViewController],
2337
- let splashVC = oldController.splashViewControllerInstance {
2338
- print("[SplashScreen2Service] rootViewController changed - Reusing existing splash screen instance")
2339
-
2340
- // 更新字典中的引用
2341
- splashScreenControllers.removeValue(forKey: oldRootViewController)
2342
- splashScreenControllers[newRootViewController] = oldController
2343
-
2344
- // 从旧的父控制器分离
2345
- splashVC.view.removeFromSuperview()
2346
- splashVC.willMove(toParent: nil)
2347
- if let oldParent = splashVC.parent {
2348
- splashVC.removeFromParent()
2349
- }
2350
-
2351
- // 添加到新的 rootViewController
2352
- newRootViewController.addChild(splashVC)
2353
- newRootViewController.view.addSubview(splashVC.view)
2354
- splashVC.view.translatesAutoresizingMaskIntoConstraints = false
2355
- NSLayoutConstraint.activate([
2356
- splashVC.view.topAnchor.constraint(equalTo: newRootViewController.view.topAnchor),
2357
- splashVC.view.leadingAnchor.constraint(equalTo: newRootViewController.view.leadingAnchor),
2358
- splashVC.view.trailingAnchor.constraint(equalTo: newRootViewController.view.trailingAnchor),
2359
- splashVC.view.bottomAnchor.constraint(equalTo: newRootViewController.view.bottomAnchor)
2360
- ])
2361
- newRootViewController.view.bringSubviewToFront(splashVC.view)
2362
- splashVC.didMove(toParent: newRootViewController)
2363
- splashVC.view.isHidden = false
2364
- splashVC.view.alpha = 1.0
2365
-
2366
- // 迁移完成后,确保隐私弹框被隐藏(如果用户已同意)
2367
- splashVC.ensurePrivacyDialogHidden()
2368
-
2369
- print("[SplashScreen2Service] rootViewController changed - Splash screen reused successfully")
2370
- } else if let oldRootViewController = observingRootViewController,
2371
- splashScreenControllers[oldRootViewController] == nil {
2372
- // 旧 rootViewController 没有记录,说明之前没有成功创建 splash screen
2373
- print("[SplashScreen2Service] rootViewController changed - No existing splash screen to reuse, creating new one")
2374
- showSplashScreenFor(newRootViewController)
2375
- }
2376
-
2377
- // 先移除旧的监听器
2378
- removeRootViewControllerListener()
2379
-
2380
- // 重新添加监听器(这会设置新的 observingRootViewController 并显示 splash screen)
2381
- // 注意:addRootViewControllerListener() 内部会调用 showSplashScreenFor,所以不需要在这里单独调用
2382
- // 但如果 globalPreventAutoHide 为 true,且已经迁移了 splash screen,不需要重新添加监听器
2383
- if !globalPreventAutoHide || splashScreenControllers[newRootViewController] == nil {
2384
- addRootViewControllerListener()
2385
- } else {
2386
- print("[SplashScreen2Service] rootViewController changed - globalPreventAutoHide is true and splash screen already migrated, skipping addRootViewControllerListener")
2387
- // 仍然需要更新 observingRootViewController 和添加监听器
2388
- if let window = UIApplication.shared.keyWindow {
2389
- window.addObserver(self, forKeyPath: "rootViewController", options: .new, context: nil)
2390
- newRootViewController.addObserver(self, forKeyPath: "view", options: .new, context: nil)
2391
- observingRootViewController = newRootViewController
2392
- // 确保 splash screen 在最上层且可见
2393
- if let controller = splashScreenControllers[newRootViewController],
2394
- let splashVC = controller.splashViewControllerInstance {
2395
- splashVC.view.isHidden = false
2396
- splashVC.view.alpha = 1.0
2397
- newRootViewController.view.bringSubviewToFront(splashVC.view)
2398
- print("[SplashScreen2Service] rootViewController changed - Brought migrated splash screen to front and ensured visibility")
2399
- }
2400
- }
2401
- }
2402
- }
2403
- } else if let rootViewController = object as? UIViewController, keyPath == "view" {
2404
- if let newView = change?[.newKey] as? UIView,
2405
- let viewController = newView.next as? UIViewController {
2406
- print("[SplashScreen2Service] view changed for viewController: \\(viewController)")
2407
- print("[SplashScreen2Service] view changed - globalPreventAutoHide: \\(globalPreventAutoHide)")
2408
-
2409
- // 如果 globalPreventAutoHide 为 true,确保现有的 splash screen 保持显示
2410
- if globalPreventAutoHide {
2411
- if let controller = splashScreenControllers[viewController] {
2412
- print("[SplashScreen2Service] view changed - globalPreventAutoHide is true, ensuring splash screen is visible")
2413
- if let splashVC = controller.splashViewControllerInstance {
2414
- splashVC.view.isHidden = false
2415
- splashVC.view.alpha = 1.0
2416
- viewController.view.bringSubviewToFront(splashVC.view)
2417
- }
2418
- return
2419
- } else {
2420
- // 如果 globalPreventAutoHide 为 true 但没有 splash screen,创建一个
2421
- print("[SplashScreen2Service] view changed - globalPreventAutoHide is true but no splash screen, creating one")
2422
- showSplashScreenFor(viewController)
2423
- return
2424
- }
2425
- }
2426
-
2427
- // 只有当 view 真正加载完成时才重新显示 splash screen
2428
- // 避免在 view 创建过程中重复调用
2429
- if viewController.view.superview != nil && splashScreenControllers[viewController] == nil {
2430
- print("[SplashScreen2Service] View loaded, showing splash screen")
2431
- showSplashScreenFor(viewController)
2432
- } else if splashScreenControllers[viewController] != nil {
2433
- print("[SplashScreen2Service] Splash screen already exists for this view controller, skipping")
2434
- }
2435
- }
2436
- }
2437
- }
2438
- }
2439
-
2440
- // 类似 EXSplashScreenViewController,管理单个 splash screen 的显示和隐藏
2441
- public class SplashScreen2ViewController {
2442
- private weak var splashViewController: SplashScreen2ViewController?
2443
- private var autoHideEnabled: Bool = true
2444
- private var splashScreenShown: Bool = false
2445
- private var appContentAppeared: Bool = false
2446
-
2447
- // 添加一个属性来访问 splashViewController,用于迁移
2448
- var splashViewControllerInstance: SplashScreen2ViewController? {
2449
- return splashViewController
2450
- }
2451
-
2452
- init(splashViewController: SplashScreen2ViewController) {
2453
- self.splashViewController = splashViewController
2454
- }
2455
-
2456
- func show() {
2457
- guard Thread.isMainThread else {
2458
- DispatchQueue.main.async { [weak self] in
2459
- self?.show()
2460
- }
2461
- return
2462
- }
2463
-
2464
- guard let splashVC = splashViewController else { return }
2465
-
2466
- print("[SplashScreen2ViewController] show() called")
2467
- print("[SplashScreen2ViewController] show() - splashVC.view.isHidden: \\(splashVC.view.isHidden)")
2468
- print("[SplashScreen2ViewController] show() - splashVC.view.alpha: \\(splashVC.view.alpha)")
2469
- print("[SplashScreen2ViewController] show() - splashVC.view.superview: \\(String(describing: splashVC.view.superview))")
2470
- print("[SplashScreen2ViewController] show() - splashVC.view.window: \\(String(describing: splashVC.view.window))")
2471
-
2472
- // 确保 view 可见
2473
- splashVC.view.isHidden = false
2474
- splashVC.view.alpha = 1.0
2475
-
2476
- // 确保 WebView 也可见
2477
- for subview in splashVC.view.subviews {
2478
- if let webView = subview as? WKWebView {
2479
- webView.isHidden = false
2480
- webView.alpha = 1.0
2481
- print("[SplashScreen2ViewController] show() - WebView visibility set: isHidden=\\(webView.isHidden), alpha=\\(webView.alpha)")
2482
- }
2483
- }
2484
-
2485
- // 确保在最上层
2486
- if let parent = splashVC.parent {
2487
- parent.view.bringSubviewToFront(splashVC.view)
2488
- print("[SplashScreen2ViewController] show() - Brought splashVC.view to front in parent")
2489
- } else if let superview = splashVC.view.superview {
2490
- superview.bringSubviewToFront(splashVC.view)
2491
- print("[SplashScreen2ViewController] show() - Brought splashVC.view to front in superview")
2492
- }
2493
-
2494
- // 强制布局更新
2495
- splashVC.view.setNeedsLayout()
2496
- splashVC.view.layoutIfNeeded()
2497
-
2498
- print("[SplashScreen2ViewController] show() - After show, splashVC.view.isHidden: \\(splashVC.view.isHidden)")
2499
- print("[SplashScreen2ViewController] show() - After show, splashVC.view.alpha: \\(splashVC.view.alpha)")
2500
- print("[SplashScreen2ViewController] show() - After show, splashVC.view.subviews.count: \\(splashVC.view.subviews.count)")
2501
-
2502
- splashScreenShown = true
2503
- }
2504
-
2505
- func hide(force: Bool = false) {
2506
- guard Thread.isMainThread else {
2507
- DispatchQueue.main.async { [weak self] in
2508
- self?.hide(force: force)
2509
- }
2510
- return
2511
- }
2512
-
2513
- print("[SplashScreen2ViewController] hide called, force: \\(force), autoHideEnabled: \\(autoHideEnabled)")
2514
- print("[SplashScreen2ViewController] hide - Stack trace: \\(Thread.callStackSymbols.prefix(5).joined(separator: "\\n"))")
2515
-
2516
- // 如果 preventAutoHide 被调用,且不是强制隐藏,则不执行隐藏操作
2517
- if !force && !autoHideEnabled {
2518
- print("[SplashScreen2ViewController] Auto hide is prevented, ignoring hide call (use force=true to override)")
2519
- // 确保 splash screen 仍然可见
2520
- if let splashVC = splashViewController {
2521
- splashVC.view.isHidden = false
2522
- splashVC.view.alpha = 1.0
2523
- if let parent = splashVC.parent {
2524
- parent.view.bringSubviewToFront(splashVC.view)
2525
- } else if let superview = splashVC.view.superview {
2526
- superview.bringSubviewToFront(splashVC.view)
2527
- }
2528
- }
2529
- return
2530
- }
2531
-
2532
- guard let splashVC = splashViewController else {
2533
- print("[SplashScreen2ViewController] hide - splashViewController is nil")
2534
- return
2535
- }
2536
-
2537
- print("[SplashScreen2ViewController] hide - Proceeding with hide animation")
2538
-
2539
- UIView.animate(withDuration: 0.3, animations: {
2540
- splashVC.view.alpha = 0.0
2541
- }) { _ in
2542
- print("[SplashScreen2ViewController] hide - Animation completed, removing from superview")
2543
- splashVC.view.removeFromSuperview()
2544
- splashVC.willMove(toParent: nil)
2545
- if let parent = splashVC.parent {
2546
- splashVC.removeFromParent()
2547
- }
2548
- }
2549
-
2550
- splashScreenShown = false
2551
- // 注意:只有在强制隐藏时才重置 autoHideEnabled
2552
- // 如果 preventAutoHide 被调用,autoHideEnabled 应该保持为 false
2553
- if force {
2554
- autoHideEnabled = true
2555
- }
2556
- }
2557
-
2558
- func preventAutoHide() {
2559
- print("[SplashScreen2ViewController] preventAutoHide called, autoHideEnabled: \\(autoHideEnabled)")
2560
- guard autoHideEnabled else {
2561
- print("[SplashScreen2ViewController] preventAutoHide - Already prevented, skipping")
2562
- return
2563
- }
2564
- autoHideEnabled = false
2565
- print("[SplashScreen2ViewController] preventAutoHide - Set autoHideEnabled to false")
2566
- }
2567
-
2568
- func needsHideOnAppContentDidAppear() -> Bool {
2569
- if !appContentAppeared && autoHideEnabled {
2570
- appContentAppeared = true
2571
- return true
2572
- }
2573
- return false
2574
- }
2575
-
2576
- func needsShowOnAppContentWillReload() -> Bool {
2577
- if !appContentAppeared {
2578
- // 注意:如果 preventAutoHide 已经被调用,不应该重置 autoHideEnabled
2579
- // 只有在 preventAutoHide 没有被调用时才重置
2580
- if autoHideEnabled {
2581
- autoHideEnabled = true
2582
- }
2583
- appContentAppeared = false
2584
- return true
2585
- }
2586
- return false
2587
- }
2588
- }
2589
- `;
2590
- try {
2591
- fs.writeFileSync(servicePath, serviceContent);
2592
- }
2593
- catch (error) {
2594
- console.error(`[expo-splash-screen2] Failed to generate SplashScreen2Service.swift:`, error);
2595
- throw error;
2596
- }
2597
- }
2598
- function generateSplashScreen2ViewController(bundleIdentifier, projectRoot, iosPath, backgroundColor, projectName) {
2599
- const targetDir = path.join(iosPath, projectName);
2600
- if (!fs.existsSync(targetDir)) {
2601
- fs.mkdirSync(targetDir, { recursive: true });
2602
- }
2603
- const viewControllerPath = path.join(targetDir, 'SplashScreen2ViewController.swift');
2604
- const viewControllerContent = `import UIKit
2605
- import WebKit
2606
-
2607
- // 简化版 SplashScreen2ViewController,只用于 WebView 显示 HTML
2608
- // 参考 expo-splash-screen 的架构,但使用 WebView 显示 HTML
2609
- public class SplashScreen2ViewController: UIViewController {
2610
- private var webView: WKWebView?
2611
- private var webViewContainer: UIView?
2612
- private let userDefaults = UserDefaults.standard
2613
-
2614
- public static weak var appDelegate: AppDelegateProtocol?
2615
-
2616
- public override func viewDidLoad() {
2617
- super.viewDidLoad()
2618
-
2619
- print("[SplashScreen2ViewController] viewDidLoad called")
2620
- print("[SplashScreen2ViewController] viewDidLoad - view.frame: \\(view.frame)")
2621
- print("[SplashScreen2ViewController] viewDidLoad - view.bounds: \\(view.bounds)")
2622
- print("[SplashScreen2ViewController] viewDidLoad - view.superview: \\(String(describing: view.superview))")
2623
- print("[SplashScreen2ViewController] viewDidLoad - view.window: \\(String(describing: view.window))")
2624
-
2625
- // 设置 view 的背景色为传入的 backgroundColor
2626
- // 将十六进制颜色转换为 UIColor
2627
- let hexColor = "${backgroundColor}".uppercased().replacingOccurrences(of: "#", with: "")
2628
- if hexColor.count == 6 {
2629
- let r = CGFloat(Int(hexColor.prefix(2), radix: 16) ?? 0) / 255.0
2630
- let g = CGFloat(Int(String(hexColor.dropFirst(2).prefix(2)), radix: 16) ?? 0) / 255.0
2631
- let b = CGFloat(Int(hexColor.suffix(2), radix: 16) ?? 0) / 255.0
2632
- view.backgroundColor = UIColor(red: r, green: g, blue: b, alpha: 1.0)
2633
- } else {
2634
- view.backgroundColor = .clear
2635
- }
2636
-
2637
- // 确保全屏显示
2638
- edgesForExtendedLayout = .all
2639
-
2640
- // 如果 view 已经有 superview,确保 frame 正确
2641
- if let superview = view.superview {
2642
- view.frame = superview.bounds
2643
- print("[SplashScreen2ViewController] viewDidLoad - Updated view.frame to superview.bounds: \\(view.frame)")
2644
- } else {
2645
- // 如果没有 superview,使用屏幕尺寸
2646
- view.frame = UIScreen.main.bounds
2647
- print("[SplashScreen2ViewController] viewDidLoad - Set view.frame to UIScreen.main.bounds: \\(view.frame)")
2648
- }
2649
-
2650
- // 注册通知监听
2651
- NotificationCenter.default.addObserver(
2652
- self,
2653
- selector: #selector(handlePreventAutoHide),
2654
- name: NSNotification.Name("SplashHtmlPreventAutoHide"),
2655
- object: nil
2656
- )
2657
- NotificationCenter.default.addObserver(
2658
- self,
2659
- selector: #selector(handleHide),
2660
- name: NSNotification.Name("SplashHtmlHide"),
2661
- object: nil
2662
- )
2663
-
2664
- setupWebView()
2665
- }
2666
-
2667
- deinit {
2668
- // 移除通知监听
2669
- NotificationCenter.default.removeObserver(self)
2670
- }
2671
-
2672
- @objc private func handlePreventAutoHide() {
2673
- print("[SplashScreen2ViewController] handlePreventAutoHide called")
2674
- // 通过 SplashScreen2Service 防止自动隐藏
2675
- // 需要传递 parent view controller(通常是 rootViewController)
2676
- if let parentVC = parent {
2677
- SplashScreen2Service.shared.preventAutoHideFor(parentVC)
2678
- } else if let rootVC = view.window?.rootViewController {
2679
- SplashScreen2Service.shared.preventAutoHideFor(rootVC)
2680
- } else {
2681
- print("[SplashScreen2ViewController] handlePreventAutoHide - No parent or rootViewController found")
2682
- }
2683
- }
2684
-
2685
- @objc private func handleHide() {
2686
- print("[SplashScreen2ViewController] handleHide called")
2687
- // 通过 SplashScreen2Service 隐藏开屏
2688
- // 需要传递 parent view controller(通常是 rootViewController)
2689
- if let parentVC = parent {
2690
- SplashScreen2Service.shared.hideSplashScreenFor(parentVC, force: true)
2691
- } else if let rootVC = view.window?.rootViewController {
2692
- SplashScreen2Service.shared.hideSplashScreenFor(rootVC, force: true)
2693
- } else {
2694
- print("[SplashScreen2ViewController] handleHide - No parent or rootViewController found")
2695
- }
2696
- }
2697
-
2698
- public override func viewWillAppear(_ animated: Bool) {
2699
- super.viewWillAppear(animated)
2700
-
2701
- // 强制设置 view 的 frame 为全屏
2702
- if let window = view.window {
2703
- view.frame = window.bounds
2704
- } else {
2705
- view.frame = UIScreen.main.bounds
2706
- }
2707
-
2708
- print("[SplashScreen2ViewController] viewWillAppear - view.frame: \\(view.frame)")
2709
- print("[SplashScreen2ViewController] viewWillAppear - view.bounds: \\(view.bounds)")
2710
- print("[SplashScreen2ViewController] viewWillAppear - UIScreen.main.bounds: \\(UIScreen.main.bounds)")
2711
- }
2712
-
2713
- public override func viewDidLayoutSubviews() {
2714
- super.viewDidLayoutSubviews()
2715
-
2716
- // 强制设置 view 的 frame 为全屏
2717
- if let window = view.window {
2718
- view.frame = window.bounds
2719
- } else {
2720
- view.frame = UIScreen.main.bounds
2721
- }
2722
-
2723
- // 确保 webView 也是全屏
2724
- if let webView = webView {
2725
- webView.frame = view.bounds
2726
- print("[SplashScreen2ViewController] viewDidLayoutSubviews - webView.frame: \\(webView.frame)")
2727
- print("[SplashScreen2ViewController] viewDidLayoutSubviews - webView.bounds: \\(webView.bounds)")
2728
- }
2729
- }
2730
-
2731
- private func setupWebView() {
2732
- let config = WKWebViewConfiguration()
2733
- config.preferences.javaScriptEnabled = true
2734
- config.allowsInlineMediaPlayback = true
2735
- config.mediaTypesRequiringUserActionForPlayback = []
2736
-
2737
- // 添加 JavaScript 接口
2738
- let contentController = WKUserContentController()
2739
- contentController.add(self, name: "agreePrivacyPolicy")
2740
- contentController.add(self, name: "disagreePrivacyPolicy")
2741
- contentController.add(self, name: "openPrivacyPolicy")
2742
- config.userContentController = contentController
2743
-
2744
- webView = WKWebView(frame: view.bounds, configuration: config)
2745
- // 设置透明背景
2746
- webView?.backgroundColor = .clear
2747
- webView?.isOpaque = false
2748
- webView?.scrollView.backgroundColor = .clear
2749
- webView?.scrollView.showsVerticalScrollIndicator = false
2750
- webView?.scrollView.showsHorizontalScrollIndicator = false
2751
- webView?.scrollView.bounces = false
2752
- webView?.scrollView.isScrollEnabled = false
2753
- webView?.allowsLinkPreview = false
2754
- webView?.allowsBackForwardNavigationGestures = false
2755
-
2756
- if #available(iOS 11.0, *) {
2757
- webView?.scrollView.contentInsetAdjustmentBehavior = .never
2758
- }
2759
-
2760
- guard let webView = webView else { return }
2761
-
2762
- // 确保 view 的 frame 正确
2763
- if view.frame == .zero {
2764
- if let superview = view.superview {
2765
- view.frame = superview.bounds
2766
- } else {
2767
- view.frame = UIScreen.main.bounds
2768
- }
2769
- print("[SplashScreen2ViewController] setupWebView - view.frame was zero, updated to: \\(view.frame)")
2770
- }
2771
-
2772
- // 确保 webView 的 frame 正确
2773
- if webView.frame == .zero {
2774
- webView.frame = view.bounds
2775
- print("[SplashScreen2ViewController] setupWebView - webView.frame was zero, updated to: \\(webView.frame)")
2776
- }
2777
-
2778
- // 将 WebView 添加到 view 上
2779
- view.addSubview(webView)
2780
- print("[SplashScreen2ViewController] setupWebView - WebView added to view, view.subviews.count: \\(view.subviews.count)")
2781
-
2782
- webView.translatesAutoresizingMaskIntoConstraints = false
2783
- NSLayoutConstraint.activate([
2784
- webView.topAnchor.constraint(equalTo: view.topAnchor),
2785
- webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
2786
- webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
2787
- webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
2788
- ])
2789
-
2790
- // 强制布局更新
2791
- view.setNeedsLayout()
2792
- view.layoutIfNeeded()
2793
- webView.setNeedsLayout()
2794
- webView.layoutIfNeeded()
2795
-
2796
- // 打印尺寸信息用于调试
2797
- print("[SplashScreen2ViewController] setupWebView - view.frame: \\(view.frame)")
2798
- print("[SplashScreen2ViewController] setupWebView - view.bounds: \\(view.bounds)")
2799
- print("[SplashScreen2ViewController] setupWebView - view.superview: \\(String(describing: view.superview))")
2800
- print("[SplashScreen2ViewController] setupWebView - view.window: \\(String(describing: view.window))")
2801
- print("[SplashScreen2ViewController] setupWebView - view.isHidden: \\(view.isHidden)")
2802
- print("[SplashScreen2ViewController] setupWebView - view.alpha: \\(view.alpha)")
2803
- print("[SplashScreen2ViewController] setupWebView - webView.frame: \\(webView.frame)")
2804
- print("[SplashScreen2ViewController] setupWebView - webView.bounds: \\(webView.bounds)")
2805
- print("[SplashScreen2ViewController] setupWebView - webView.superview: \\(String(describing: webView.superview))")
2806
- print("[SplashScreen2ViewController] setupWebView - webView.isHidden: \\(webView.isHidden)")
2807
- print("[SplashScreen2ViewController] setupWebView - webView.alpha: \\(webView.alpha)")
2808
- print("[SplashScreen2ViewController] setupWebView - webView.isOpaque: \\(webView.isOpaque)")
2809
- print("[SplashScreen2ViewController] setupWebView - webView.backgroundColor: \\(String(describing: webView.backgroundColor))")
2810
- print("[SplashScreen2ViewController] setupWebView - UIScreen.main.bounds: \\(UIScreen.main.bounds)")
2811
-
2812
- // 确保 WebView 可见
2813
- webView.isHidden = false
2814
- webView.alpha = 1.0
2815
- view.isHidden = false
2816
- view.alpha = 1.0
2817
-
2818
- print("[SplashScreen2ViewController] setupWebView - After setting visibility, webView.isHidden: \\(webView.isHidden), webView.alpha: \\(webView.alpha)")
2819
-
2820
- webView.navigationDelegate = self
2821
-
2822
- // 加载 HTML 文件
2823
- if let htmlPath = Bundle.main.path(forResource: "index", ofType: "html") {
2824
- if let htmlString = try? String(contentsOfFile: htmlPath, encoding: .utf8) {
2825
- let baseURL = URL(fileURLWithPath: htmlPath).deletingLastPathComponent()
2826
- webView.loadHTMLString(htmlString, baseURL: baseURL)
2827
- }
2828
- }
2829
- }
2830
-
2831
- private func handleAgreePrivacyPolicy() {
2832
- userDefaults.set(true, forKey: "isAuth")
2833
- userDefaults.synchronize()
2834
-
2835
- let hideDialogJS = """
2836
- (function() {
2837
- try {
2838
- if (typeof closePrivacyDialog === 'function') {
2839
- closePrivacyDialog();
2840
- }
2841
- if (typeof hidePrivacyDialog === 'function') {
2842
- hidePrivacyDialog();
2843
- }
2844
- return true;
2845
- } catch (e) {
2846
- return false;
2298
+ const entries = fs.readdirSync(iosPath, { withFileTypes: true });
2299
+ const projectDir = entries
2300
+ .filter((e) => e.isDirectory())
2301
+ .map((e) => e.name)
2302
+ .find((d) => fs.existsSync(path.join(iosPath, `${d}.xcodeproj`)));
2303
+ if (projectDir) {
2304
+ targetDir = path.join(iosPath, projectDir);
2305
+ }
2847
2306
  }
2848
- })();
2849
- """
2850
-
2851
- let startReactNative: () -> Void = {
2852
- DispatchQueue.main.async {
2853
- if let appDelegate = SplashScreen2ViewController.appDelegate {
2854
- appDelegate.startReactNativeIfNeeded()
2307
+ catch { }
2308
+ console.log(`[expo-splash-screen2] [iOS] targetDir: ${targetDir}`);
2309
+ if (!fs.existsSync(targetDir)) {
2310
+ fs.mkdirSync(targetDir, { recursive: true });
2855
2311
  }
2856
- }
2857
- }
2858
-
2859
- // 先尝试通过 JS 隐藏弹框,等结果返回后再启动 RN
2860
- webView?.evaluateJavaScript(hideDialogJS) { _, error in
2861
- if let error = error {
2862
- print("[SplashScreen2ViewController] hide dialog JS error: \\(error)")
2863
- }
2864
- startReactNative()
2865
- } ?? startReactNative()
2866
- }
2867
-
2868
- // 公共方法:确保隐私弹框被隐藏(用于迁移后重新注入状态)
2869
- public func ensurePrivacyDialogHidden() {
2870
- let isAuth = userDefaults.bool(forKey: "isAuth")
2871
- guard isAuth else {
2872
- print("[SplashScreen2ViewController] ensurePrivacyDialogHidden - isAuth is false, skipping")
2873
- return
2874
- }
2875
-
2876
- let hideDialogJS = """
2877
- (function() {
2878
- try {
2879
- // 重新注入 isAuth 状态
2880
- window.isAuth = true;
2881
- if (window.iOS) {
2882
- window.iOS.getIsAuth = function() {
2883
- return true;
2884
- };
2885
- }
2886
-
2887
- // 确保弹框被隐藏
2888
- if (typeof closePrivacyDialog === 'function') {
2889
- closePrivacyDialog();
2890
- }
2891
- if (typeof hidePrivacyDialog === 'function') {
2892
- hidePrivacyDialog();
2893
- }
2894
-
2895
- // 强制设置弹框状态为隐藏(如果 HTML 中有状态变量)
2896
- if (typeof setShowModal === 'function') {
2897
- setShowModal(false);
2898
- }
2899
-
2900
- return true;
2901
- } catch (e) {
2902
- console.error('Error hiding privacy dialog:', e);
2903
- return false;
2904
- }
2905
- })();
2906
- """
2907
-
2908
- webView?.evaluateJavaScript(hideDialogJS) { result, error in
2909
- if let error = error {
2910
- print("[SplashScreen2ViewController] ensurePrivacyDialogHidden JS error: \\(error)")
2911
- } else {
2912
- print("[SplashScreen2ViewController] ensurePrivacyDialogHidden - Privacy dialog hidden successfully")
2913
- }
2914
- }
2915
- }
2916
-
2917
- private func handleDisagreePrivacyPolicy() {
2918
- exit(0)
2919
- }
2920
-
2921
- private func handleOpenPrivacyPolicy(url: String) {
2922
- DispatchQueue.main.async {
2923
- let privacyVC = SplashScreen2PrivacyPolicyViewController()
2924
- privacyVC.url = url
2925
- self.present(privacyVC, animated: true, completion: nil)
2926
- }
2927
- }
2928
- }
2929
-
2930
- extension SplashScreen2ViewController: WKNavigationDelegate {
2931
- public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
2932
- // 确保 WebView 和 view 都可见
2933
- webView.isHidden = false
2934
- webView.alpha = 1.0
2935
- view.isHidden = false
2936
- view.alpha = 1.0
2937
-
2938
- // 确保在最上层
2939
- if let superview = view.superview {
2940
- superview.bringSubviewToFront(view)
2941
- }
2942
-
2943
- // 获取 isAuth 状态并注入到 HTML
2944
- let isAuth = userDefaults.bool(forKey: "isAuth")
2945
-
2946
- // 注入 CSS 确保内容全屏显示,但不覆盖 HTML 中的背景色
2947
- let css = """
2948
- (function() {
2949
- var style = document.createElement('style');
2950
- style.innerHTML = "html, body { margin: 0 !important; padding: 0 !important; width: 100% !important; height: 100% !important; overflow: hidden !important; position: fixed !important; top: 0 !important; left: 0 !important; }";
2951
- document.head.appendChild(style);
2952
- })();
2953
- """
2954
- webView.evaluateJavaScript(css, completionHandler: nil)
2955
-
2956
- // 延迟执行,确保 HTML 中的函数已经定义
2957
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
2958
- guard let self = self else { return }
2959
-
2960
- // 首先检查 HTML 中是否存在隐私协议相关的函数
2961
- let checkPrivacyFunctionsJS = """
2962
- (function() {
2963
- var hasPrivacyFunctions =
2964
- typeof checkAuthStatus === 'function' ||
2965
- typeof showPrivacyDialog === 'function' ||
2966
- typeof hidePrivacyDialog === 'function' ||
2967
- typeof closePrivacyDialog === 'function' ||
2968
- typeof agreePrivacyPolicy === 'function' ||
2969
- typeof disagreePrivacyPolicy === 'function';
2970
- return hasPrivacyFunctions;
2971
- })();
2972
- """
2973
-
2974
- self.webView?.evaluateJavaScript(checkPrivacyFunctionsJS) { result, error in
2975
- if let error = error {
2976
- print("[SplashScreen2ViewController] Error checking privacy functions: \\(error)")
2977
- // 如果检查出错,默认认为没有隐私协议,设置 isAuth 为 true
2978
- self.userDefaults.set(true, forKey: "isAuth")
2979
- // 直接启动 React Native
2980
- SplashScreen2ViewController.appDelegate?.startReactNativeIfNeeded()
2981
- return
2312
+ const htmlContent = fs.readFileSync(sourcePath, 'utf-8');
2313
+ const htmlDir = path.dirname(sourcePath);
2314
+ console.log(`[expo-splash-screen2] [iOS] htmlDir: ${htmlDir}`);
2315
+ const imagePaths = extractImagePaths(htmlContent, htmlDir);
2316
+ console.log(`[expo-splash-screen2] [iOS] extractImagePaths found: ${imagePaths.length} images`);
2317
+ imagePaths.forEach(({ original, absolute }) => {
2318
+ console.log(`[expo-splash-screen2] [iOS] - original: ${original}, absolute: ${absolute}`);
2319
+ });
2320
+ const assetsDir = path.join(htmlDir, 'assets');
2321
+ console.log(`[expo-splash-screen2] [iOS] checking assetsDir: ${assetsDir}`);
2322
+ console.log(`[expo-splash-screen2] [iOS] assetsDir exists: ${fs.existsSync(assetsDir)}`);
2323
+ if (fs.existsSync(assetsDir) && fs.statSync(assetsDir).isDirectory()) {
2324
+ const allFiles = fs.readdirSync(assetsDir);
2325
+ console.log(`[expo-splash-screen2] [iOS] assetsDir all files: ${allFiles.join(', ')}`);
2326
+ const imageFiles = allFiles.filter(f => /\.(png|jpg|jpeg|gif|svg|webp|ico)$/i.test(f));
2327
+ console.log(`[expo-splash-screen2] [iOS] assetsDir image files: ${imageFiles.join(', ')}`);
2328
+ imageFiles.forEach(imgFile => {
2329
+ const srcPath = path.join(assetsDir, imgFile);
2330
+ const absolutePath = srcPath;
2331
+ imagePaths.push({ original: `./assets/${imgFile}`, absolute: absolutePath });
2332
+ console.log(`[expo-splash-screen2] [iOS] added from assets: ./assets/${imgFile} -> ${absolutePath}`);
2333
+ });
2982
2334
  }
2983
-
2984
- let hasPrivacyFunctions = (result as? Bool) ?? false
2985
- print("[SplashScreen2ViewController] HTML has privacy functions: \\(hasPrivacyFunctions)")
2986
-
2987
- if !hasPrivacyFunctions {
2988
- // 如果 HTML 中没有隐私协议相关代码,默认 isAuth true
2989
- print("[SplashScreen2ViewController] No privacy functions found, setting isAuth to true")
2990
-
2991
- // isAuth 设置为 true 并保存
2992
- self.userDefaults.set(true, forKey: "isAuth")
2993
-
2994
- // 注入 isAuth=true 到 HTML,并执行 isAuth=true 的逻辑
2995
- let jsCode = """
2996
- (function() {
2997
- console.log('No privacy functions found, setting isAuth to true');
2998
- // 注入 isAuth 状态为 true
2999
- window.isAuth = true;
3000
- window.iOS = {
3001
- getIsAuth: function() {
3002
- return true;
3003
- }
3004
- };
3005
-
3006
- // 执行 isAuth=true 的逻辑:隐藏弹框(如果存在)
3007
- if (typeof hidePrivacyDialog === 'function') {
3008
- console.log('Calling hidePrivacyDialog');
3009
- hidePrivacyDialog();
3010
- }
3011
- if (typeof closePrivacyDialog === 'function') {
3012
- console.log('Calling closePrivacyDialog');
3013
- closePrivacyDialog();
3014
- }
3015
- })();
3016
- """
3017
- self.webView?.evaluateJavaScript(jsCode, completionHandler: { result, error in
3018
- if let error = error {
3019
- print("[SplashScreen2ViewController] Error evaluating JavaScript: \\(error)")
3020
- }
3021
- // 启动 React Native(isAuth=true 的逻辑)
3022
- print("[SplashScreen2ViewController] Starting React Native with isAuth=true")
3023
- SplashScreen2ViewController.appDelegate?.startReactNativeIfNeeded()
3024
- })
3025
- } else {
3026
- // 如果存在隐私协议相关代码,按原来的逻辑处理
3027
- let jsCode = """
3028
- (function() {
3029
- // 注入 isAuth 状态
3030
- window.isAuth = \\(isAuth);
3031
- window.iOS = {
3032
- getIsAuth: function() {
3033
- return \\(isAuth);
2335
+ const imagesDir = path.join(htmlDir, 'images');
2336
+ console.log(`[expo-splash-screen2] [iOS] checking imagesDir: ${imagesDir}`);
2337
+ console.log(`[expo-splash-screen2] [iOS] imagesDir exists: ${fs.existsSync(imagesDir)}`);
2338
+ if (fs.existsSync(imagesDir) && fs.statSync(imagesDir).isDirectory()) {
2339
+ const imageFiles = fs.readdirSync(imagesDir).filter(f => /\.(png|jpg|jpeg|gif|svg|webp|ico)$/i.test(f));
2340
+ console.log(`[expo-splash-screen2] [iOS] imagesDir image files: ${imageFiles.join(', ')}`);
2341
+ imageFiles.forEach(imgFile => {
2342
+ const srcPath = path.join(imagesDir, imgFile);
2343
+ const absolutePath = srcPath;
2344
+ imagePaths.push({ original: `./images/${imgFile}`, absolute: absolutePath });
2345
+ });
2346
+ }
2347
+ console.log(`[expo-splash-screen2] [iOS] total imagePaths after scanning: ${imagePaths.length}`);
2348
+ const imagePathMap = new Map();
2349
+ imagePaths.forEach(({ original, absolute }) => {
2350
+ const fileNameWithHash = path.basename(absolute);
2351
+ const fileNameWithoutHash = removeHashFromFileName(fileNameWithHash);
2352
+ const newPath = `./${fileNameWithoutHash}`;
2353
+ console.log(`[expo-splash-screen2] [iOS] processing: ${original} -> ${newPath} (file: ${absolute})`);
2354
+ const targetImagePath = path.join(targetDir, fileNameWithoutHash);
2355
+ console.log(`[expo-splash-screen2] [iOS] copying to: ${targetImagePath}`);
2356
+ if (fs.existsSync(absolute)) {
2357
+ fs.copyFileSync(absolute, targetImagePath);
2358
+ console.log(`[expo-splash-screen2] [iOS] copied successfully: ${fileNameWithoutHash}`);
2359
+ imagePathMap.set(original, newPath);
2360
+ const normalizedOriginal = original.startsWith('./') ? original : `./${original}`;
2361
+ imagePathMap.set(normalizedOriginal, newPath);
2362
+ if (original.startsWith('./')) {
2363
+ imagePathMap.set(original.substring(2), newPath);
3034
2364
  }
3035
- };
3036
-
3037
- // 根据 isAuth 状态决定显隐弹框
3038
- if (window.isAuth) {
3039
- // 如果已同意,隐藏弹框
3040
- if (typeof hidePrivacyDialog === 'function') {
3041
- hidePrivacyDialog();
2365
+ if (original.startsWith('./images/')) {
2366
+ imagePathMap.set(original, newPath);
2367
+ imagePathMap.set(original.substring(2), newPath);
2368
+ imagePathMap.set(original.substring(10), newPath);
3042
2369
  }
3043
- } else {
3044
- // 如果未同意,显示弹框
3045
- if (typeof checkAuthStatus === 'function') {
3046
- checkAuthStatus();
3047
- } else if (typeof showPrivacyDialog === 'function') {
3048
- showPrivacyDialog();
2370
+ if (original.startsWith('./assets/')) {
2371
+ imagePathMap.set(original, newPath);
2372
+ imagePathMap.set(original.substring(2), newPath);
2373
+ const originalFileName = path.basename(original);
2374
+ if (originalFileName !== fileNameWithoutHash) {
2375
+ imagePathMap.set(original.replace(originalFileName, fileNameWithoutHash), newPath);
2376
+ }
3049
2377
  }
3050
- }
3051
- })();
3052
- """
3053
- self.webView?.evaluateJavaScript(jsCode, completionHandler: { result, error in
3054
- if let error = error {
3055
- print("[SplashScreen2ViewController] Error evaluating JavaScript: \\(error)")
3056
- } else {
3057
- // 在 JavaScript 执行完成后,根据 isAuth 状态决定是否启动 React Native
3058
- if isAuth {
3059
- SplashScreen2ViewController.appDelegate?.startReactNativeIfNeeded()
3060
- }
3061
2378
  }
3062
- })
3063
- }
3064
- }
3065
- }
3066
- }
3067
- }
3068
-
3069
- extension SplashScreen2ViewController: WKScriptMessageHandler {
3070
- public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
3071
- switch message.name {
3072
- case "agreePrivacyPolicy":
3073
- handleAgreePrivacyPolicy()
3074
- case "disagreePrivacyPolicy":
3075
- handleDisagreePrivacyPolicy()
3076
- case "openPrivacyPolicy":
3077
- if let url = message.body as? String {
3078
- handleOpenPrivacyPolicy(url: url)
3079
- }
3080
- default:
3081
- break
3082
- }
3083
- }
3084
- }
3085
- `;
3086
- try {
3087
- fs.writeFileSync(viewControllerPath, viewControllerContent);
3088
- }
3089
- catch (error) {
3090
- console.error(`[expo-splash-screen2] Failed to generate SplashScreen2ViewController.swift:`, error);
3091
- throw error;
3092
- }
3093
- }
3094
- function generateSplashScreen2PrivacyPolicyViewController(bundleIdentifier, projectRoot, iosPath, projectName) {
3095
- const targetDir = path.join(iosPath, projectName);
3096
- if (!fs.existsSync(targetDir)) {
3097
- fs.mkdirSync(targetDir, { recursive: true });
3098
- }
3099
- const viewControllerPath = path.join(targetDir, 'SplashScreen2PrivacyPolicyViewController.swift');
3100
- const viewControllerContent = ios_1.IOS_TEMPLATES.privacyPolicyViewController;
3101
- try {
3102
- fs.writeFileSync(viewControllerPath, viewControllerContent);
3103
- }
3104
- catch (error) {
3105
- console.error(`[expo-splash-screen2] Failed to generate SplashScreen2PrivacyPolicyViewController.swift:`, error);
3106
- }
3107
- }
3108
- function generateSplashScreen2Module(bundleIdentifier, projectRoot, iosPath, projectName) {
3109
- const targetDir = path.join(iosPath, projectName);
3110
- if (!fs.existsSync(targetDir)) {
3111
- fs.mkdirSync(targetDir, { recursive: true });
3112
- }
3113
- const modulePath = path.join(targetDir, 'SplashScreen2Module.swift');
3114
- const moduleContent = ios_1.IOS_TEMPLATES.splashHtmlModule;
3115
- try {
3116
- fs.writeFileSync(modulePath, moduleContent);
2379
+ });
2380
+ let updatedHtmlContent = htmlContent;
2381
+ updatedHtmlContent = updatedHtmlContent.replace(/<img([^>]+)src\s*=\s*["']([^"']+)["']/gi, (match, attrs, srcPath) => {
2382
+ if (srcPath.startsWith('http') || srcPath.startsWith('data:')) {
2383
+ return match;
2384
+ }
2385
+ const newPath = imagePathMap.get(srcPath) || imagePathMap.get(`./${srcPath}`) || imagePathMap.get(srcPath.replace(/^\.\//, ''));
2386
+ if (newPath) {
2387
+ return `<img${attrs}src="${newPath}"`;
2388
+ }
2389
+ return match;
2390
+ });
2391
+ updatedHtmlContent = updatedHtmlContent.replace(/url\s*\(\s*["']?([^"')]+)["']?\s*\)/gi, (match, urlPath) => {
2392
+ if (urlPath.startsWith('http') || urlPath.startsWith('data:')) {
2393
+ return match;
2394
+ }
2395
+ const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico'];
2396
+ const lowerPath = urlPath.toLowerCase();
2397
+ if (!imageExtensions.some(ext => lowerPath.includes(ext))) {
2398
+ return match;
2399
+ }
2400
+ const newPath = imagePathMap.get(urlPath) || imagePathMap.get(`./${urlPath}`) || imagePathMap.get(urlPath.replace(/^\.\//, ''));
2401
+ if (newPath) {
2402
+ return `url("${newPath}")`;
2403
+ }
2404
+ return match;
2405
+ });
2406
+ updatedHtmlContent = updatedHtmlContent.replace(/(["'])(\.\/images\/[^"']+\.(png|jpg|jpeg|gif|svg|webp|ico))(["'])/gi, (match, quote1, imgPath, ext, quote2) => {
2407
+ const fileName = path.basename(imgPath);
2408
+ const fileNameWithoutHash = removeHashFromFileName(fileName);
2409
+ const newPath = imagePathMap.get(`./images/${fileName}`) ||
2410
+ imagePathMap.get(`./images/${fileNameWithoutHash}`) ||
2411
+ imagePathMap.get(`./${fileName}`) ||
2412
+ imagePathMap.get(`./${fileNameWithoutHash}`) ||
2413
+ `./${fileNameWithoutHash}`;
2414
+ return `${quote1}${newPath}${quote2}`;
2415
+ });
2416
+ updatedHtmlContent = updatedHtmlContent.replace(/(["'])(\.\/assets\/[^"']+\.(png|jpg|jpeg|gif|svg|webp|ico))(["'])/gi, (match, quote1, imgPath, ext, quote2) => {
2417
+ const fileName = path.basename(imgPath);
2418
+ const fileNameWithoutHash = removeHashFromFileName(fileName);
2419
+ const newPath = imagePathMap.get(imgPath) ||
2420
+ imagePathMap.get(`./assets/${imgPath.substring(2)}`) ||
2421
+ `./${fileNameWithoutHash}`;
2422
+ return `${quote1}${newPath}${quote2}`;
2423
+ });
2424
+ updatedHtmlContent = updatedHtmlContent.replace(/(\.\/images\/[^\s"'`;,\)]+\.(png|jpg|jpeg|gif|svg|webp|ico))/gi, (match, imgPath) => {
2425
+ const fileName = path.basename(imgPath);
2426
+ const fileNameWithoutHash = removeHashFromFileName(fileName);
2427
+ const newPath = imagePathMap.get(`./images/${fileName}`) ||
2428
+ imagePathMap.get(`./images/${fileNameWithoutHash}`) ||
2429
+ imagePathMap.get(`./${fileName}`) ||
2430
+ imagePathMap.get(`./${fileNameWithoutHash}`) ||
2431
+ `./${fileNameWithoutHash}`;
2432
+ return newPath;
2433
+ });
2434
+ updatedHtmlContent = updatedHtmlContent.replace(/(\.\/assets\/[^\s"'`;,\)]+\.(png|jpg|jpeg|gif|svg|webp|ico))/gi, (match, imgPath) => {
2435
+ const fileName = path.basename(imgPath);
2436
+ const fileNameWithoutHash = removeHashFromFileName(fileName);
2437
+ const newPath = imagePathMap.get(imgPath) ||
2438
+ imagePathMap.get(`./assets/${imgPath.substring(2)}`) ||
2439
+ `./${fileNameWithoutHash}`;
2440
+ return newPath;
2441
+ });
2442
+ const targetPath = path.join(targetDir, 'index.html');
2443
+ fs.writeFileSync(targetPath, updatedHtmlContent, 'utf-8');
3117
2444
  }
3118
2445
  catch (error) {
3119
- console.error(`[expo-splash-screen2] Failed to generate SplashScreen2Module.swift:`, error);
3120
- throw error;
2446
+ console.error(`[expo-splash-screen2] Error copying HTML file for iOS: ${error}`);
3121
2447
  }
3122
2448
  }
3123
2449
  function copyBackgroundImageToIOS(projectRoot, backgroundImagePath, iosPath, projectName) {
@@ -3943,17 +3269,6 @@ function modifyAppDelegate(content, backgroundColor = '#ffffff') {
3943
3269
  }
3944
3270
  return content;
3945
3271
  }
3946
- function hexToStoryboardColor(hex) {
3947
- hex = hex.replace('#', '');
3948
- if (hex.length === 3) {
3949
- hex = hex.split('').map(char => char + char).join('');
3950
- }
3951
- const r = parseInt(hex.substring(0, 2), 16) / 255;
3952
- const g = parseInt(hex.substring(2, 4), 16) / 255;
3953
- const b = parseInt(hex.substring(4, 6), 16) / 255;
3954
- const a = hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1;
3955
- return { red: r, green: g, blue: b, alpha: a };
3956
- }
3957
3272
  function copyIconToIOS(projectRoot, iconPath, iosPath, projectName) {
3958
3273
  try {
3959
3274
  const sourcePath = path.resolve(projectRoot, iconPath);
@@ -4395,106 +3710,9 @@ const withIosSplashScreenStoryboard = (config, action) => {
4395
3710
  action
4396
3711
  });
4397
3712
  };
4398
- const withIosSplashScreenStoryboardBaseMod = (config) => {
4399
- return config_plugins_1.BaseMods.withGeneratedBaseMods(config, {
4400
- platform: 'ios',
4401
- saveToInternal: true,
4402
- skipEmptyMod: false,
4403
- providers: {
4404
- [STORYBOARD_MOD_NAME]: config_plugins_1.BaseMods.provider({
4405
- isIntrospective: true,
4406
- async getFilePath({ modRequest }) {
4407
- return path.join(modRequest.platformProjectRoot, modRequest.projectName || 'MyNewExpoSplashDemo', STORYBOARD_FILE_PATH);
4408
- },
4409
- async read(filePath) {
4410
- try {
4411
- const contents = await fs.promises.readFile(filePath, 'utf8');
4412
- try {
4413
- const { Parser } = require('xml2js');
4414
- const xml = await new Parser().parseStringPromise(contents);
4415
- if (!xml || !xml.document) {
4416
- console.warn('[expo-splash-screen2] Invalid XML structure: xml.document is missing, using template');
4417
- return getTemplateAsync();
4418
- }
4419
- if (!xml.document.scenes || !Array.isArray(xml.document.scenes) || !xml.document.scenes[0]) {
4420
- console.warn('[expo-splash-screen2] Invalid XML structure: scenes missing, using template');
4421
- return getTemplateAsync();
4422
- }
4423
- if (!xml.document.resources) {
4424
- xml.document.resources = [{}];
4425
- }
4426
- if (!Array.isArray(xml.document.resources)) {
4427
- xml.document.resources = [xml.document.resources];
4428
- }
4429
- if (!xml.document.resources[0]) {
4430
- xml.document.resources[0] = {};
4431
- }
4432
- if (!xml.document.resources[0].image) {
4433
- xml.document.resources[0].image = [];
4434
- }
4435
- if (!xml.document.resources[0].namedColor) {
4436
- xml.document.resources[0].namedColor = [];
4437
- }
4438
- return xml;
4439
- }
4440
- catch (parseError) {
4441
- console.warn(`[expo-splash-screen2] Failed to parse XML: ${parseError}, using template`);
4442
- return getTemplateAsync();
4443
- }
4444
- }
4445
- catch (readError) {
4446
- console.warn(`[expo-splash-screen2] Failed to read storyboard file: ${readError}, using template`);
4447
- return getTemplateAsync();
4448
- }
4449
- },
4450
- async write(filePath, { modResults, modRequest: { introspect } }) {
4451
- if (introspect) {
4452
- return;
4453
- }
4454
- await fs.promises.writeFile(filePath, toString(modResults));
4455
- }
4456
- })
4457
- }
4458
- });
4459
- };
4460
3713
  function modifyInfoPlist(plist) {
4461
3714
  return plist;
4462
3715
  }
4463
- const addSplashSourceFiles = (proj, projectName, iosPath, projectRoot) => {
4464
- const sourceFiles = [
4465
- 'SplashScreen2ViewController.swift',
4466
- 'SplashScreen2PrivacyPolicyViewController.swift',
4467
- 'SplashScreen2Module.swift',
4468
- 'SplashScreen2Service.swift'
4469
- ];
4470
- sourceFiles.forEach((fileName) => {
4471
- const filePath = path.join(iosPath, projectName, fileName);
4472
- if (!fs.existsSync(filePath)) {
4473
- console.warn(`[expo-splash-screen2] Swift file ${fileName} does not exist at ${filePath}, skipping`);
4474
- return;
4475
- }
4476
- const relativeFilePath = `${projectName}/${fileName}`;
4477
- if (proj.hasFile(relativeFilePath) || proj.hasFile(`../${projectName}/${fileName}`)) {
4478
- return;
4479
- }
4480
- try {
4481
- const target = proj.getFirstTarget();
4482
- if (!target) {
4483
- console.error(`[expo-splash-screen2] Failed to find target for source file ${fileName}`);
4484
- return;
4485
- }
4486
- const groupUuid = proj.findPBXGroupKey({ name: projectName });
4487
- if (!groupUuid) {
4488
- console.error(`[expo-splash-screen2] Failed to find group "${projectName}" for source file ${fileName}`);
4489
- return;
4490
- }
4491
- proj.addSourceFile(`${projectName}/${fileName}`, { target: target.uuid }, groupUuid);
4492
- }
4493
- catch (error) {
4494
- console.error(`[expo-splash-screen2] Error adding source file ${fileName}:`, error);
4495
- }
4496
- });
4497
- };
4498
3716
  const addSplashResourceFiles = (proj, projectName, iosPath, projectRoot, pluginConfig, config) => {
4499
3717
  const resourceFiles = ['index.html'];
4500
3718
  if (config?.icon) {
@@ -4748,6 +3966,10 @@ function setupImageMode(config, pluginConfig) {
4748
3966
  stylesJSON.resources.style = modifyStylesForImageMode(stylesJSON.resources.style);
4749
3967
  return config;
4750
3968
  });
3969
+ config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
3970
+ config.modResults = modifyAndroidManifestForImageMode(config.modResults, packageName);
3971
+ return config;
3972
+ });
4751
3973
  config = (0, config_plugins_1.withMainActivity)(config, (config) => {
4752
3974
  const imageResourceName = 'splash_background_image';
4753
3975
  config.modResults.contents = modifyMainActivityForImageMode(config.modResults.contents, packageName, imageResourceName);