Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 47 additions & 30 deletions app/src/main/java/space/karrarnazim/ConsoleFlow/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,13 @@ class MainActivity : AppCompatActivity() {
"yahoo.com", "yandex.com"
)

private val LOCALHOST_HOSTS = setOf("localhost", "127.0.0.1", "::1", "10.0.2.2")

private fun isLocalhostHost(host: String?): Boolean {
val normalized = host?.lowercase().orEmpty()
return normalized in LOCALHOST_HOSTS
}

// ── مجموعات التبويبات والتبويب النشط ───────────────────────────────────
private var tabGroups = mutableListOf<TabGroup>()
private var activeGroupId = 0
Expand Down Expand Up @@ -614,13 +621,7 @@ class MainActivity : AppCompatActivity() {
}

private fun currentSearchEngineIconRes(): Int {
return when {
prefsManager.searchEngine.contains("google") -> R.drawable.ic_engine_google
prefsManager.searchEngine.contains("duckduckgo") -> R.drawable.ic_engine_duckduckgo
prefsManager.searchEngine.contains("bing") -> R.drawable.ic_engine_bing
prefsManager.searchEngine.contains("brave") -> R.drawable.ic_engine_brave
else -> R.drawable.ic_engine_google
}
return if (prefsManager.searchEngineIsCustom) R.drawable.ic_find else searchEngineIconRes(prefsManager.searchEngine)
}

private fun setTopBarVisible(visible: Boolean, immediate: Boolean = false) {
Expand Down Expand Up @@ -1313,23 +1314,31 @@ class MainActivity : AppCompatActivity() {
if (idx < 0) return

val wasActive = tab.id == activeTabId
val closingWebView = webViews.remove(tab.id)

tab.ramThumbnail?.recycle()
tab.ramThumbnail = null
tab.faviconBitmap = null
ioExecutor.execute { tabThumbnailFile(tab.id).delete() }

webViews[tab.id]?.let { wv ->
if (webViewContainer.indexOfChild(wv) >= 0) {
webViewContainer.removeView(wv)
group.tabs.removeAt(idx)

fun destroyClosedTabWebView() {
closingWebView?.let { wv ->
runCatching {
if (webViewContainer.indexOfChild(wv) >= 0) {
webViewContainer.removeView(wv)
}
}
runCatching { wv.stopLoading() }
runCatching { wv.clearHistory() }
runCatching { wv.removeAllViews() }
runCatching { wv.destroy() }
}
wv.destroy()
webViews.remove(tab.id)
}

group.tabs.removeAt(idx)

if (group.tabs.isEmpty()) {
activeTabId = 0
destroyClosedTabWebView()
openNewTab(HOME_URL)
return
}
Expand All @@ -1338,7 +1347,9 @@ class MainActivity : AppCompatActivity() {
val fallbackTab = group.tabs.getOrNull(maxOf(0, idx - 1)) ?: group.tabs.first()
activeTabId = fallbackTab.id
switchToTab(fallbackTab)
destroyClosedTabWebView()
} else {
destroyClosedTabWebView()
sanitizeActiveTabSelection()
refreshTabsRecycler() // DiffUtil يتولى الـ animation
savePersistentTabs()
Expand Down Expand Up @@ -1392,7 +1403,7 @@ class MainActivity : AppCompatActivity() {

private fun captureAndStoreThumbnail(onComplete: (() -> Unit)? = null) {
val wv = currentWebView
if (wv == null || wv.width <= 0 || wv.height <= 0) {
if (wv == null || wv.width <= 0 || wv.height <= 0 || !wv.isAttachedToWindow) {
currentGroup?.tabs?.find { it.id == activeTabId }?.let {
if (isHomeUrl(it.url)) {
it.hasThumbnail = true
Expand All @@ -1404,7 +1415,7 @@ class MainActivity : AppCompatActivity() {
}

val tabId = activeTabId
val currentUrl = wv.url ?: HOME_URL
val currentUrl = runCatching { wv.url ?: HOME_URL }.getOrDefault(HOME_URL)
val file = tabThumbnailFile(tabId)
val tabRef = currentGroup?.tabs?.find { it.id == tabId }

Expand All @@ -1413,18 +1424,18 @@ class MainActivity : AppCompatActivity() {
}

try {
val homeLike = isHomeUrl(wv.url)
val homeLike = isHomeUrl(currentUrl)
val bitmap = if (homeLike) {
getHomePreviewBitmap()
} else {
val scale = 0.3f
val w = (wv.width * scale).toInt()
val h = (wv.height * scale).toInt()
val w = maxOf(1, (wv.width * scale).toInt())
val h = maxOf(1, (wv.height * scale).toInt())
val bmp = Bitmap.createBitmap(w, h, Bitmap.Config.RGB_565)
val canvas = Canvas(bmp)
canvas.scale(scale, scale)
canvas.translate(-wv.scrollX.toFloat(), -wv.scrollY.toFloat())
wv.draw(canvas)
runCatching { wv.draw(canvas) }
bmp
}

Expand Down Expand Up @@ -1570,6 +1581,7 @@ class MainActivity : AppCompatActivity() {
if (prefsManager.getBoolean("disable_intercept", false)) return null

val host = request.url.host ?: ""
if (isLocalhostHost(host)) return null
if (NO_INTERCEPT_DOMAINS.any { host == it || host.endsWith(".$it") }) return null

if (request.isForMainFrame && request.method == "GET" && url.startsWith("http")) {
Expand All @@ -1594,12 +1606,13 @@ class MainActivity : AppCompatActivity() {
)

val erudaTags = if (prefsManager.consoleEnabled) {
"<script src=\"https://eruda.local/eruda.js\"></script>" +
"<script>(function(){if(window.__erudaInited){" +
"try{eruda.show();window.__cfConsoleEnabled=true;}catch(e){};return;}" +
"try{eruda.init();window.__erudaInited=true;window.__cfConsoleEnabled=true;" +
"}catch(e){}})()</script>"
} else ""
"<script src=\"https://eruda.local/eruda.js\"></script>" +
"<script>(function(){if(window.__erudaInited){" +
"try{eruda.show();window.__cfConsoleEnabled=true;}catch(e){};return;}" +
"try{eruda.init();window.__erudaInited=true;window.__cfConsoleEnabled=true;" +
"}catch(e){}})()</script>"
} else ""


val customJsTag = prefsManager.customJs.takeIf { it.isNotEmpty() }
?.let { "<script>$it</script>" } ?: ""
Expand All @@ -1621,6 +1634,7 @@ class MainActivity : AppCompatActivity() {
return super.shouldInterceptRequest(view, request)
}


override fun onReceivedError(view: WebView, req: WebResourceRequest, err: WebResourceError) {
if (req.isForMainFrame) {
runOnUiThread { showErrorOverlay(req.url.toString()) }
Expand Down Expand Up @@ -1822,10 +1836,12 @@ class MainActivity : AppCompatActivity() {
// ─────────────────────────────────────────────────────────────────────────

private fun navigateTo(input: String) {
val trimmed = input.trim()
val finalUrl = when {
input.startsWith("http://") || input.startsWith("https://") -> input
Patterns.WEB_URL.matcher(input).matches() -> "https://$input"
else -> prefsManager.searchEngine + URLEncoder.encode(input, "utf-8")
trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("file:") -> trimmed
isLocalhostUrl(trimmed) -> "http://$trimmed"
Patterns.WEB_URL.matcher(trimmed).matches() -> "https://$trimmed"
else -> buildSearchUrl(prefsManager.searchEngine, trimmed)
}
loadUrlInstantly(finalUrl)
}
Expand Down Expand Up @@ -2255,6 +2271,7 @@ class MainActivity : AppCompatActivity() {
}
}


// ─────────────────────────────────────────────────────────────────────────
// جسر JavaScript
// ─────────────────────────────────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ class PrefsManager(context: Context) {
get() = prefs.getString("search_engine", "https://www.google.com/search?q=")!!
set(value) = prefs.edit().putString("search_engine", value).apply()

var searchEngineIsCustom: Boolean
get() = prefs.getBoolean("search_engine_is_custom", false)
set(value) = prefs.edit().putBoolean("search_engine_is_custom", value).apply()

var customJs: String
get() = prefs.getString("custom_js", "")!!
set(value) = prefs.edit().putString("custom_js", value).apply()

var consoleEnabled: Boolean
get() = prefs.getBoolean("console_enabled", true)
get() = prefs.getBoolean("console_enabled", false)
set(value) = prefs.edit().putBoolean("console_enabled", value).apply()

// ✅ الدالة المضافة حديثًا لحل خطأ "Unresolved reference: getBoolean"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package space.karrarnazim.ConsoleFlow

import android.net.Uri
import android.util.Patterns
import android.webkit.URLUtil
import androidx.annotation.DrawableRes
import java.net.URLEncoder

enum class SearchEngineKind {
GOOGLE,
DUCKDUCKGO,
BING,
BRAVE,
CUSTOM
}


private val LOCALHOST_HOSTS = setOf("localhost", "127.0.0.1", "::1", "10.0.2.2")

fun isLocalhostUrl(input: String?): Boolean {
val value = input.orEmpty().trim().lowercase()
if (value.isEmpty()) return false
val host = runCatching {
val normalized = if (value.contains("://")) value else "http://$value"
Uri.parse(normalized).host.orEmpty().lowercase()
}.getOrDefault("")
return host in LOCALHOST_HOSTS
}

fun normalizeNavigationInput(input: String): String {
val trimmed = input.trim()
if (trimmed.isEmpty()) return trimmed
return when {
trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("file:") -> trimmed
isLocalhostUrl(trimmed) -> if (trimmed.startsWith("localhost") || trimmed.startsWith("127.") || trimmed.startsWith("10.0.2.2") || trimmed.startsWith("::1")) {
"http://$trimmed"
} else {
trimmed
}
Patterns.WEB_URL.matcher(trimmed).matches() -> "https://$trimmed"
else -> trimmed
}
}


fun resolveSearchEngineKind(engineUrl: String?): SearchEngineKind {
val url = engineUrl.orEmpty().lowercase()
val host = runCatching { Uri.parse(url).host.orEmpty().lowercase() }.getOrDefault("")
return when {
host.contains("google.") || url.contains("google.com/search") -> SearchEngineKind.GOOGLE
host.contains("duckduckgo.com") || url.contains("duckduckgo.com/?q=") -> SearchEngineKind.DUCKDUCKGO
host.contains("bing.com") || url.contains("bing.com/search?q=") -> SearchEngineKind.BING
host.contains("search.brave.com") || url.contains("search.brave.com/search?q=") -> SearchEngineKind.BRAVE
else -> SearchEngineKind.CUSTOM
}
}

fun searchEngineDisplayName(engineUrl: String?): String {
return when (resolveSearchEngineKind(engineUrl)) {
SearchEngineKind.GOOGLE -> "Google"
SearchEngineKind.DUCKDUCKGO -> "DuckDuckGo"
SearchEngineKind.BING -> "Bing"
SearchEngineKind.BRAVE -> "Brave"
SearchEngineKind.CUSTOM -> "Custom"
}
}

@DrawableRes
fun searchEngineIconRes(engineUrl: String?): Int {
return when (resolveSearchEngineKind(engineUrl)) {
SearchEngineKind.GOOGLE -> R.drawable.ic_engine_google
SearchEngineKind.DUCKDUCKGO -> R.drawable.ic_engine_duckduckgo
SearchEngineKind.BING -> R.drawable.ic_engine_bing
SearchEngineKind.BRAVE -> R.drawable.ic_engine_brave
SearchEngineKind.CUSTOM -> R.drawable.ic_find
}
}

fun buildSearchUrl(engineUrl: String, query: String): String {
val encoded = URLEncoder.encode(query, "utf-8")
val template = engineUrl.trim()
return when {
template.contains("%s") -> template.replace("%s", encoded)
template.contains("{query}") -> template.replace("{query}", encoded)
template.endsWith("=") || template.endsWith("?") || template.endsWith("&") -> template + encoded
URLUtil.isValidUrl(template) && !template.contains("?") -> "$template?q=$encoded"
else -> template + encoded
}
}
Loading
Loading