forked from Tadrisino/MathApp_frontend
Native splash held while connecting; offline shows the same picture with a connecting line; web splash hidden inside the app
Inside the app the web splash is invisible with the new APK (the system splash is still on top) and icon-only for old APKs, so there is no second picture to switch to. Offline or a 5xx on the main frame: system splash released onto the same icon (288dp drawable) with 'در حال اتصال به اینترنت…', retried every 5s. A slow-but-online load shows nothing extra. Debug build compiles; not run on a device. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package com.tadrisino.app
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -11,7 +12,14 @@ import android.os.VibrationEffect
|
|||||||
import android.os.Vibrator
|
import android.os.Vibrator
|
||||||
import android.os.VibratorManager
|
import android.os.VibratorManager
|
||||||
import android.util.Log
|
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.JavascriptInterface
|
||||||
|
import android.webkit.WebResourceResponse
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
@@ -46,11 +54,60 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private lateinit var webView: WebView
|
private lateinit var webView: WebView
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* اسپلشِ سیستم تا وقتی true نشده روی صفحه میماند. فرانت وقتی اولین صفحهی واقعی (ورود/خانه) کشیده شد
|
* اسپلشِ سیستم تا وقتی true نشده روی صفحه میماند، هرچقدر اتصال کند باشد (خواستِ مالک): فرانت وقتی
|
||||||
* `TadrisinoAndroid.appReady()` را صدا میزند. اگر هرگز نرسید (سایت بالا نیامد، JS خطا داد) بعد از
|
* اولین صفحهی واقعی (ورود/خانه) کشیده شد `TadrisinoAndroid.appReady()` را صدا میزند.
|
||||||
* SPLASH_MAX_MS رها میشود تا کاربر بهجای اسپلشِ بیانتها صفحهی خطای WebView را ببیند، نه چیزِ دیگر.
|
* فقط دو حالت زودتر رهایش میکند: صفحهی اصلی بالا نیامد (آفلاین/خطای سرور → [showOffline]، با همان
|
||||||
|
* تصویر و یک جمله)، یا صفحه لود شد ولی تا [READY_WATCHDOG_MS] appReady نیامد (خطای JS؛ لاگ میشود).
|
||||||
*/
|
*/
|
||||||
@Volatile private var webReady = false
|
@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 payment: Payment? = null
|
||||||
private var connection: Connection? = null
|
private var connection: Connection? = null
|
||||||
private val webHost: String = Uri.parse(BuildConfig.WEB_URL).host ?: ""
|
private val webHost: String = Uri.parse(BuildConfig.WEB_URL).host ?: ""
|
||||||
@@ -70,13 +127,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen().setKeepOnScreenCondition { !webReady }
|
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)
|
super.onCreate(savedInstanceState)
|
||||||
webView = WebView(this)
|
webView = WebView(this)
|
||||||
setContentView(webView)
|
offlineView = buildOfflineView()
|
||||||
|
setContentView(FrameLayout(this).apply {
|
||||||
|
addView(webView)
|
||||||
|
addView(offlineView)
|
||||||
|
})
|
||||||
|
|
||||||
webView.settings.javaScriptEnabled = true
|
webView.settings.javaScriptEnabled = true
|
||||||
webView.settings.domStorageEnabled = true // توکنِ ورود در localStorage است
|
webView.settings.domStorageEnabled = true // توکنِ ورود در localStorage است
|
||||||
@@ -86,9 +143,21 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// صداهای کوتاهِ رابط (Web Audio) بعد از پاسخِ سرور پخش میشوند، نه درست در لحظهی لمس.
|
// صداهای کوتاهِ رابط (Web Audio) بعد از پاسخِ سرور پخش میشوند، نه درست در لحظهی لمس.
|
||||||
webView.settings.mediaPlaybackRequiresUserGesture = false
|
webView.settings.mediaPlaybackRequiresUserGesture = false
|
||||||
webView.webViewClient = object : WebViewClient() {
|
webView.webViewClient = object : WebViewClient() {
|
||||||
// صفحهی اصلی بالا نیامد (نبودِ شبکه): اسپلش را رها کن تا خطای WebView دیده شود، نه ۱۰ ثانیه انتظار.
|
// صفحهی اصلی بالا نیامد (بیاینترنت یا خطای سرور): همان تصویر با «در حال اتصال»، و هر چند ثانیه دوباره.
|
||||||
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: android.webkit.WebResourceError) {
|
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
|
@JavascriptInterface
|
||||||
fun appReady() {
|
fun appReady() {
|
||||||
webReady = true
|
webReady = true
|
||||||
|
runOnUiThread { hideOffline() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** فرانت با این میفهمد درگاه داخلِ اپ باز میشود (isPaymentPage)، نه در مرورگرِ بیرونی. */
|
/** فرانت با این میفهمد درگاه داخلِ اپ باز میشود (isPaymentPage)، نه در مرورگرِ بیرونی. */
|
||||||
@@ -251,6 +321,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "Tadrisino"
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
index.html
12
index.html
@@ -14,10 +14,22 @@
|
|||||||
Vazirmatn بعد از CSS میرسد و عوضشدنِ فونت خودش یک پرش است. -->
|
Vazirmatn بعد از CSS میرسد و عوضشدنِ فونت خودش یک پرش است. -->
|
||||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||||
<link rel="preload" as="image" href="/app-icon.png" />
|
<link rel="preload" as="image" href="/app-icon.png" />
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var b = window.TadrisinoAndroid;
|
||||||
|
if (b) document.documentElement.className = typeof b.appReady === 'function' ? 'in-app' : 'in-old-app';
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.boot-splash { position: fixed; inset: 0; z-index: 50; display: flex; flex-direction: column;
|
.boot-splash { position: fixed; inset: 0; z-index: 50; display: flex; flex-direction: column;
|
||||||
align-items: center; justify-content: center; gap: 20px; background: #fff; }
|
align-items: center; justify-content: center; gap: 20px; background: #fff; }
|
||||||
.boot-splash img { width: 144px; height: 144px; display: block; }
|
.boot-splash img { width: 144px; height: 144px; display: block; }
|
||||||
|
/* داخلِ اپِ اندروید اسپلشِ بومی روی صفحه است و سایت زیرش بار میشود؛ اسپلشِ وب نباید یک بار دیگر
|
||||||
|
«عوض شدن» نشان دهد. APKِ تازه (appReady دارد): اسپلشِ وب نامرئی است. APKِ قدیمی: فقط آیکون،
|
||||||
|
بینام، تا کمتر از اسپلشِ سیستم فرق کند. */
|
||||||
|
.in-app .boot-splash > * { visibility: hidden; }
|
||||||
|
.in-old-app .boot-splash span { display: none; }
|
||||||
|
.in-old-app .boot-splash img { width: 160px; height: 160px; }
|
||||||
.boot-splash span { font: 900 34px/1.2 system-ui, -apple-system, sans-serif; color: #1a1a2e; }
|
.boot-splash span { font: 900 34px/1.2 system-ui, -apple-system, sans-serif; color: #1a1a2e; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Reference in New Issue
Block a user