diff --git a/android/app/src/main/java/com/thefeed/android/AndroidBridge.kt b/android/app/src/main/java/com/thefeed/android/AndroidBridge.kt index b25a9d2..330b423 100644 --- a/android/app/src/main/java/com/thefeed/android/AndroidBridge.kt +++ b/android/app/src/main/java/com/thefeed/android/AndroidBridge.kt @@ -28,6 +28,27 @@ class AndroidBridge(private val activity: Activity) { @JavascriptInterface fun isAndroid(): Boolean = true + // ===== App lifecycle (back-button confirmation) ===== + + @JavascriptInterface + fun minimizeApp() { + Handler(Looper.getMainLooper()).post { + activity.moveTaskToBack(true) + } + } + + @JavascriptInterface + fun killApp() { + Handler(Looper.getMainLooper()).post { + activity.stopService(Intent(activity, ThefeedService::class.java)) + activity.finishAndRemoveTask() + // Belt-and-braces: ensure the JVM exits even if a stray + // foreground notification or pending Intent keeps the + // process alive after finishAndRemoveTask(). + android.os.Process.killProcess(android.os.Process.myPid()) + } + } + // ===== Language ===== @JavascriptInterface diff --git a/android/app/src/main/java/com/thefeed/android/MainActivity.kt b/android/app/src/main/java/com/thefeed/android/MainActivity.kt index 51ee32f..59077f2 100644 --- a/android/app/src/main/java/com/thefeed/android/MainActivity.kt +++ b/android/app/src/main/java/com/thefeed/android/MainActivity.kt @@ -155,19 +155,11 @@ class MainActivity : ComponentActivity() { private fun registerBackHandler() { onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - // Check if the chat view is open (mobile nav). If yes, go back - // to the channel list. If already on the channel list, minimize. - // Uses openSidebar() directly instead of webView.goBack() to avoid - // history-stack mismatches that can leave the UI stuck mid-transition. - webView.evaluateJavascript( - "(document.getElementById('app').classList.contains('chat-open')).toString()" - ) { result -> - if (result.trim('"') == "true") { - webView.evaluateJavascript("openSidebar(); history.back();", null) - } else { - moveTaskToBack(true) - } - } + // Delegate to JS — it knows about open lightboxes, modals, + // and chat-open state, and can show the close-confirmation + // dialog at app root. JS calls back through AndroidBridge + // (minimizeApp / killApp) when the user picks an option. + webView.evaluateJavascript("window.handleAndroidBack && window.handleAndroidBack();", null) } }) } diff --git a/internal/web/static/index.html b/internal/web/static/index.html index b0b5ee5..3a07929 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -2606,6 +2606,10 @@ media_slow_only: 'رله گیتهاب در دسترس نیست. دانلود از مسیر DNS خیلی کند است. ادامه میدهی؟', media_rate_limited: 'محدودیت گیتهاب پر شد ({n} دقیقه تا ریست). از مسیر DNS ادامه میدهیم.', media_rate_limited_short: 'محدودیت تعداد درخواست گیتهاب', + close_confirm: 'بستن thefeed؟', + close_cancel: 'نبند', + close_background: 'بستن، اما در پسزمینه فعال بمونه', + close_kill: 'بستن و توقف کامل سرویس', media_size_mismatch: 'سایز فایل با چیزی که سرور گفته بود نمیخونه', clear_cache: 'پاک کردن کش', clear_cache_btn: '🗑 پاک کردن کش', cache_cleared: 'کش پاک شد!', saved_resolvers_title: 'شروع سریع', @@ -2775,6 +2779,10 @@ media_slow_only: 'GitHub relay is unavailable for this file. The DNS path is very slow. Download anyway?', media_rate_limited: 'GitHub rate limit hit ({n} min until reset). Falling back to slow DNS path.', media_rate_limited_short: 'GitHub rate limit', + close_confirm: 'Close thefeed?', + close_cancel: "Don't close", + close_background: 'Close UI but keep running in background', + close_kill: 'Close and stop the service', media_size_mismatch: 'Downloaded size doesn\'t match the manifest', clear_cache: 'Clear Cache', clear_cache_btn: '🗑 Clear Cache', cache_cleared: 'Cache cleared!', saved_resolvers_title: 'Quick Start', @@ -4818,6 +4826,55 @@ // way of getting around blob-URL / Web-Share limitations on Android. var androidBridge = (typeof window !== 'undefined' && window.Android) ? window.Android : null; + // Single entry point invoked by MainActivity.kt's back-press handler. + // Returns nothing — the JS side resolves the action itself, calling + // back into the bridge for "minimize"/"kill" if the user picks one + // of those from the close-confirmation dialog. + window.handleAndroidBack = async function () { + if (closeMediaLightbox()) return; + var openModal = document.querySelector('.modal-overlay.active'); + if (openModal) { openModal.classList.remove('active'); return; } + if (mobileQuery.matches && document.getElementById('app').classList.contains('chat-open')) { + openSidebar(); + return; + } + // At app root — confirm before closing/killing the process. + var choice = await showCloseConfirm(); + if (choice === 'background' && androidBridge && androidBridge.minimizeApp) { + androidBridge.minimizeApp(); + } else if (choice === 'kill' && androidBridge && androidBridge.killApp) { + androidBridge.killApp(); + } + // 'cancel' or unknown → stay in the app. + }; + + function showCloseConfirm() { + return new Promise(function (resolve) { + var existing = document.getElementById('closeConfirmModal'); + if (existing) existing.remove(); + var overlay = document.createElement('div'); + overlay.id = 'closeConfirmModal'; + overlay.className = 'modal-overlay active'; + overlay.innerHTML = + '
' + + esc(t('close_confirm') || 'Close thefeed?') + '
' + + '