diff --git a/android/app/src/main/java/com/tadrisino/app/MainActivity.kt b/android/app/src/main/java/com/tadrisino/app/MainActivity.kt index d0a756e..32322da 100644 --- a/android/app/src/main/java/com/tadrisino/app/MainActivity.kt +++ b/android/app/src/main/java/com/tadrisino/app/MainActivity.kt @@ -2,6 +2,7 @@ package com.tadrisino.app import android.annotation.SuppressLint import android.content.Intent +import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Bundle @@ -11,7 +12,14 @@ import android.os.VibrationEffect import android.os.Vibrator import android.os.VibratorManager import android.util.Log +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView import android.webkit.JavascriptInterface +import android.webkit.WebResourceResponse import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient @@ -46,11 +54,60 @@ class MainActivity : AppCompatActivity() { private lateinit var webView: WebView /** - * اسپلشِ سیستم تا وقتی true نشده روی صفحه می‌ماند. فرانت وقتی اولین صفحه‌ی واقعی (ورود/خانه) کشیده شد - * `TadrisinoAndroid.appReady()` را صدا می‌زند. اگر هرگز نرسید (سایت بالا نیامد، JS خطا داد) بعد از - * SPLASH_MAX_MS رها می‌شود تا کاربر به‌جای اسپلشِ بی‌انتها صفحه‌ی خطای WebView را ببیند، نه چیزِ دیگر. + * اسپلشِ سیستم تا وقتی true نشده روی صفحه می‌ماند، هرچقدر اتصال کند باشد (خواستِ مالک): فرانت وقتی + * اولین صفحه‌ی واقعی (ورود/خانه) کشیده شد `TadrisinoAndroid.appReady()` را صدا می‌زند. + * فقط دو حالت زودتر رهایش می‌کند: صفحه‌ی اصلی بالا نیامد (آفلاین/خطای سرور → [showOffline]، با همان + * تصویر و یک جمله)، یا صفحه لود شد ولی تا [READY_WATCHDOG_MS] appReady نیامد (خطای JS؛ لاگ می‌شود). */ @Volatile private var webReady = false + + private val handler = Handler(Looper.getMainLooper()) + private lateinit var offlineView: View + private var offlineShown = false + private val retryLoad = object : Runnable { + override fun run() { + if (!offlineShown) return + webView.loadUrl(BuildConfig.WEB_URL) + handler.postDelayed(this, RETRY_MS) + } + } + + private fun dp(v: Int): Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, v.toFloat(), resources.displayMetrics).toInt() + + /** همان تصویرِ اسپلشِ سیستم (۲۸۸dp، همان drawable) با یک جمله زیرش؛ جای آیکون با اسپلشِ سیستم یکی است. */ + private fun buildOfflineView(): View = FrameLayout(this).apply { + setBackgroundColor(Color.WHITE) + visibility = View.GONE + isClickable = true + addView(ImageView(context).apply { setImageResource(R.drawable.splash_icon) }, + FrameLayout.LayoutParams(dp(288), dp(288), Gravity.CENTER)) + addView(TextView(context).apply { + text = "در حال اتصال به اینترنت…" + setTextColor(Color.parseColor("#1a1a2e")) + setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f) + layoutDirection = View.LAYOUT_DIRECTION_RTL + gravity = Gravity.CENTER + }, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER) + .apply { topMargin = dp(150) }) + } + + private fun showOffline() { + if (!offlineShown) { + offlineShown = true + webView.visibility = View.INVISIBLE // صفحه‌ی خطای پیش‌فرضِ WebView دیده نشود + offlineView.visibility = View.VISIBLE + handler.postDelayed(retryLoad, RETRY_MS) + } + webReady = true + } + + private fun hideOffline() { + if (!offlineShown) return + offlineShown = false + handler.removeCallbacks(retryLoad) + offlineView.visibility = View.GONE + webView.visibility = View.VISIBLE + } private var payment: Payment? = null private var connection: Connection? = null private val webHost: String = Uri.parse(BuildConfig.WEB_URL).host ?: "" @@ -70,13 +127,13 @@ class MainActivity : AppCompatActivity() { @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen().setKeepOnScreenCondition { !webReady } - Handler(Looper.getMainLooper()).postDelayed({ - if (!webReady) Log.w(TAG, "web app never reported ready; releasing the splash after ${SPLASH_MAX_MS}ms") - webReady = true - }, SPLASH_MAX_MS) super.onCreate(savedInstanceState) webView = WebView(this) - setContentView(webView) + offlineView = buildOfflineView() + setContentView(FrameLayout(this).apply { + addView(webView) + addView(offlineView) + }) webView.settings.javaScriptEnabled = true webView.settings.domStorageEnabled = true // توکنِ ورود در localStorage است @@ -86,9 +143,21 @@ class MainActivity : AppCompatActivity() { // صداهای کوتاهِ رابط (Web Audio) بعد از پاسخِ سرور پخش می‌شوند، نه درست در لحظه‌ی لمس. webView.settings.mediaPlaybackRequiresUserGesture = false webView.webViewClient = object : WebViewClient() { - // صفحه‌ی اصلی بالا نیامد (نبودِ شبکه): اسپلش را رها کن تا خطای WebView دیده شود، نه ۱۰ ثانیه انتظار. + // صفحه‌ی اصلی بالا نیامد (بی‌اینترنت یا خطای سرور): همان تصویر با «در حال اتصال»، و هر چند ثانیه دوباره. override fun onReceivedError(view: WebView, request: WebResourceRequest, error: android.webkit.WebResourceError) { - if (request.isForMainFrame) webReady = true + if (request.isForMainFrame) showOffline() + } + + override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) { + if (request.isForMainFrame && errorResponse.statusCode >= 500) showOffline() + } + + override fun onPageFinished(view: WebView, url: String?) { + if (offlineShown) return // صفحه‌ی خطا هم onPageFinished می‌زند؛ تا بارِ موفق همان تصویر بماند + handler.postDelayed({ + if (!webReady) Log.e(TAG, "page loaded but appReady never came in ${READY_WATCHDOG_MS}ms; releasing the splash") + webReady = true + }, READY_WATCHDOG_MS) } // هر آدرسی بیرونِ دامنه‌ی خودمان در مرورگرِ گوشی باز می‌شود، نه داخلِ اپ؛ پس پل فقط @@ -229,6 +298,7 @@ class MainActivity : AppCompatActivity() { @JavascriptInterface fun appReady() { webReady = true + runOnUiThread { hideOffline() } } /** فرانت با این می‌فهمد درگاه داخلِ اپ باز می‌شود (isPaymentPage)، نه در مرورگرِ بیرونی. */ @@ -251,6 +321,7 @@ class MainActivity : AppCompatActivity() { companion object { private const val TAG = "Tadrisino" - private const val SPLASH_MAX_MS = 10_000L + private const val RETRY_MS = 5_000L + private const val READY_WATCHDOG_MS = 15_000L } } diff --git a/index.html b/index.html index f4576c4..0df6ad0 100644 --- a/index.html +++ b/index.html @@ -14,10 +14,22 @@ Vazirmatn بعد از CSS می‌رسد و عوض‌شدنِ فونت خودش یک پرش است. --> +