diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f384ed3..88115fc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + outputStream.write(imageBytes) + } + contentValues.clear() + contentValues.put(android.provider.MediaStore.Downloads.IS_PENDING, 0) + resolver.update(uri, contentValues, null, null) + Toast.makeText(this@MainActivity, "Файл сохранён в папке «Загрузки» (Downloads)\n$fileName", Toast.LENGTH_LONG).show() + } else { + Toast.makeText(this@MainActivity, "Download failed", Toast.LENGTH_SHORT).show() + } + } catch (e: Exception) { + Toast.makeText(this@MainActivity, "Ошибка сохранения: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + } + }, "Android") + webView.webChromeClient = object : WebChromeClient() { + override fun onConsoleMessage(message: String?, lineNumber: Int, sourceID: String?) { + message?.let { android.util.Log.d("DuckAI", it) } + } + override fun onShowFileChooser( webView: WebView?, filePathCallback: ValueCallback>?, @@ -88,6 +151,26 @@ class MainActivity : AppCompatActivity() { webView.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView?, url: String?) { view?.postDelayed({ tryFocusInput(view) }, 2000) + view?.postDelayed({ injectDownloadHandler(view) }, 1500) + } + } + + webView.setDownloadListener { url, userAgent, contentDisposition, mimeType, contentLength -> + if (url.startsWith("blob:") || url.startsWith("data:")) { + return@setDownloadListener + } else if (!url.startsWith("http://") && !url.startsWith("https://")) { + return@setDownloadListener + } else { + val fileName = android.webkit.URLUtil.guessFileName(url, contentDisposition, mimeType) ?: "download" + + val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val downloadRequest = DownloadManager.Request(Uri.parse(url)).apply { + setMimeType(mimeType) + setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName) + } + downloadManager.enqueue(downloadRequest) + Toast.makeText(this, "Downloading...", Toast.LENGTH_SHORT).show() } } } @@ -133,13 +216,84 @@ class MainActivity : AppCompatActivity() { " return 'NOT FOUND';" + "})();" ) { result -> - android.util.Log.d("DuckAI", "Focus result: " + result) if (result?.contains("NOT FOUND") == true) { view?.postDelayed({ tryFocusInput(view) }, 1000) } } } + private fun injectDownloadHandler(view: WebView?) { + view?.evaluateJavascript( + "(function() {" + + " if (window.downloadHandlerInjected) return;" + + " window.downloadHandlerInjected = true;" + + " " + + " function downloadBlobImage(src) {" + + " fetch(src)" + + " .then(res => res.blob())" + + " .then(blob => {" + + " const reader = new FileReader();" + + " reader.onloadend = function() {" + + " const base64 = reader.result.split(',')[1];" + + " const link = document.createElement('a');" + + " link.href = 'data:image/png;base64,' + base64;" + + " link.download = 'duckai_image_' + Date.now() + '.png';" + + " link.click();" + + " };" + + " reader.readAsDataURL(blob);" + + " })" + + " }" + + " " + + " function findImageSrc(btn) {" + + " let el = btn;" + + " for (let i = 0; i < 5 && el; i++) {" + + " const img = el.querySelector('img');" + + " if (img && img.src) return img.src;" + + " const sources = el.querySelectorAll('source');" + + " for (let s of sources) {" + + " if (s.src && s.src.startsWith('blob:')) return s.src;" + + " }" + + " el = el.parentElement;" + + " }" + + " const allImgs = document.querySelectorAll('img');" + + " for (let img of allImgs) {" + + " if (img.src.startsWith('blob:')) return img.src;" + + " }" + + " return null;" + + " }" + + " " + + " document.body.addEventListener('click', function(e) {" + + " const target = e.target;" + + " const btn = target.closest('button');" + + " if (!btn) return;" + + " " + + " const svg = btn.querySelector('svg');" + + " if (!svg) return;" + + " " + + " const path = svg.querySelector('path');" + + " if (!path || !path.getAttribute('d')) return;" + + " " + + " const d = path.getAttribute('d');" + + " " + + " if (d.includes('M8 .5') || d.includes('8.264')) {" + + " e.preventDefault();" + + " e.stopPropagation();" + + " " + + " const imgSrc = findImageSrc(btn);" + + " if (!imgSrc) return;" + + " " + + " if (imgSrc.startsWith('data:')) {" + + " window.Android && window.Android.downloadBase64Image && window.Android.downloadBase64Image(imgSrc);" + + " } else if (imgSrc.startsWith('blob:')) {" + + " downloadBlobImage(imgSrc);" + + " } else if (imgSrc.startsWith('http')) {" + + " window.Android && window.Android.downloadImage && window.Android.downloadImage(imgSrc);" + + " }" + + " }" + + " }, true);" + + "})();" , null) + } + private fun checkPermissionAndOpenPicker() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { openFilePicker() @@ -197,6 +351,9 @@ class MainActivity : AppCompatActivity() { window.decorView.postDelayed({ requestFocusAndShowKeyboard() }, 1200) + webView.postDelayed({ + injectDownloadHandler(webView) + }, 2000) } private fun requestFocusAndShowKeyboard() { @@ -233,6 +390,9 @@ override fun onResume() { tryFocusInput(webView) showKeyboard() }, 1500) + webView.postDelayed({ + injectDownloadHandler(webView) + }, 2000) } override fun onNewIntent(intent: Intent?) { @@ -246,6 +406,9 @@ override fun onResume() { webView.postDelayed({ requestFocusAndShowKeyboard() }, 1200) + webView.postDelayed({ + injectDownloadHandler(webView) + }, 2000) } }