diff --git a/app/build.gradle b/app/build.gradle index 5066320..1631f76 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { applicationId "com.duckai.app" - minSdk 33 + minSdk 31 targetSdk 34 versionCode 1 versionName "1.0" @@ -35,6 +35,5 @@ android { dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.activity:activity-ktx:1.8.2' implementation 'com.google.android.material:material:1.11.0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 88115fc..f384ed3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,6 @@ - - >? = null companion object { private const val BASE_URL = "https://duck.ai" } - private val filePickerLauncher = registerForActivityResult( - ActivityResultContracts.OpenMultipleDocuments() - ) { uris -> - pendingFileCallback?.onReceiveValue(uris.toTypedArray()) - pendingFileCallback = null - } - - private val permissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { granted -> - if (granted) { - openFilePicker() - } else { - pendingFileCallback?.onReceiveValue(null) - pendingFileCallback = null - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -69,7 +37,6 @@ class MainActivity : AppCompatActivity() { setSupportZoom(false) loadWithOverviewMode = true useWideViewPort = true - setAllowFileAccessFromFileURLs(false) } webView.isFocusable = true @@ -77,265 +44,19 @@ class MainActivity : AppCompatActivity() { CookieManager.getInstance().setAcceptCookie(true) CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true) - - webView.addJavascriptInterface(object { - @android.webkit.JavascriptInterface - fun downloadImage(url: String) { - runOnUiThread { - if (!url.startsWith("http://") && !url.startsWith("https://")) { - return@runOnUiThread - } - val fileName = android.webkit.URLUtil.guessFileName(url, null, null) ?: "image_${System.currentTimeMillis()}.png" - val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - val downloadRequest = DownloadManager.Request(Uri.parse(url)).apply { - setMimeType("image/png") - setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName) - } - downloadManager.enqueue(downloadRequest) - Toast.makeText(this@MainActivity, "Downloading...", Toast.LENGTH_SHORT).show() - } - } - - @android.webkit.JavascriptInterface - fun downloadBase64Image(base64Data: String) { - runOnUiThread { - try { - val base64 = base64Data.substringAfter("base64,") - val imageBytes = android.util.Base64.decode(base64, android.util.Base64.DEFAULT) - val fileName = "duckai_image_${System.currentTimeMillis()}.jpg" - - val contentValues = android.content.ContentValues().apply { - put(android.provider.MediaStore.Downloads.DISPLAY_NAME, fileName) - put(android.provider.MediaStore.Downloads.MIME_TYPE, "image/jpeg") - put(android.provider.MediaStore.Downloads.IS_PENDING, 1) - } - - val resolver = contentResolver - val uri = resolver.insert(android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) - - if (uri != null) { - resolver.openOutputStream(uri)?.use { outputStream -> - 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>?, - fileChooserParams: FileChooserParams? - ): Boolean { - pendingFileCallback = filePathCallback - checkPermissionAndOpenPicker() - return true - } - } webView.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView?, url: String?) { - view?.postDelayed({ tryFocusInput(view) }, 2000) - view?.postDelayed({ injectDownloadHandler(view) }, 1500) + view?.postDelayed({ + view.evaluateJavascript( + "setTimeout(() => {" + + " const input = document.querySelector('input[type=\"text\"], textarea[id*=\"message\"], [role=\"combobox\"]');" + + " if(input) { input.focus(); input.click(); }" + + "}, 100);" + ) { _ -> } + }, 800) } } - - 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() - } - } - } - - private fun tryFocusInput(view: WebView?) { - view?.evaluateJavascript( - "(function() {" + - " const selectors = [" + - " 'textarea[placeholder=\"Ask privately\"]'," + - " 'textarea[name=\"user-prompt\"]'," + - " 'textarea.JRDRiEf5NPKWK43sArdC'," + - " 'textarea[id*=\"message\"]'," + - " 'textarea[name*=\"message\"]'," + - " 'div[contenteditable=\"true\"]'," + - " 'div[role=\"textbox\"]'" + - " ];" + - " " + - " for (const sel of selectors) {" + - " const el = document.querySelector(sel);" + - " if (el && el.offsetParent !== null) {" + - " el.style.visibility = 'visible';" + - " el.style.display = 'block';" + - " el.scrollIntoView({block: 'center', behavior: 'instant'});" + - " el.focus();" + - " if (el.tagName === 'TEXTAREA' && el.setSelectionRange) {" + - " el.setSelectionRange(el.value?.length || 0, el.value?.length || 0);" + - " }" + - " return 'FOUND: ' + sel;" + - " }" + - " }" + - " " + - " const allTextareas = document.querySelectorAll('textarea');" + - " for (const el of allTextareas) {" + - " if (el.offsetParent !== null && el.clientHeight > 20) {" + - " el.style.visibility = 'visible';" + - " el.style.display = 'block';" + - " el.scrollIntoView({block: 'center', behavior: 'instant'});" + - " el.focus();" + - " return 'FOUND fallback: ' + el.name + ' class=' + el.className;" + - " }" + - " }" + - " " + - " return 'NOT FOUND';" + - "})();" - ) { 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() - } else { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) - == PackageManager.PERMISSION_GRANTED) { - openFilePicker() - } else { - permissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) - } - } - } - - private fun openFilePicker() { - val mimeTypes = arrayOf( - "text/plain", - "text/markdown", - "text/csv", - "application/pdf", - "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/rtf", - "application/vnd.ms-excel", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "application/vnd.oasis.opendocument.spreadsheet", - "application/vnd.ms-powerpoint", - "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "image/png", - "image/jpeg", - "image/webp", - "image/gif", - "application/x-python", - "application/javascript", - "application/java", - "application/json", - "application/xml", - "text/yaml", - "text/html", - "text/css", - "application/zip" - ) - filePickerLauncher.launch(mimeTypes) } private fun loadUrlFromIntent() { @@ -349,23 +70,9 @@ class MainActivity : AppCompatActivity() { webView.loadUrl(url) window.decorView.postDelayed({ - requestFocusAndShowKeyboard() - }, 1200) - webView.postDelayed({ - injectDownloadHandler(webView) - }, 2000) - } - - private fun requestFocusAndShowKeyboard() { - webView.requestFocus() - - // Try to focus input field - tryFocusInput(webView) - - // Show keyboard after JS - window.decorView.postDelayed({ + webView.requestFocus() showKeyboard() - }, 500) + }, 800) } private fun showKeyboard() { @@ -379,20 +86,9 @@ class MainActivity : AppCompatActivity() { super.onPause() } -override fun onResume() { + override fun onResume() { webView.onResume() super.onResume() - webView.postDelayed({ - tryFocusInput(webView) - showKeyboard() - }, 500) - webView.postDelayed({ - tryFocusInput(webView) - showKeyboard() - }, 1500) - webView.postDelayed({ - injectDownloadHandler(webView) - }, 2000) } override fun onNewIntent(intent: Intent?) { @@ -404,11 +100,9 @@ override fun onResume() { val url = "$BASE_URL/?q=${Uri.encode(query)}" webView.loadUrl(url) webView.postDelayed({ - requestFocusAndShowKeyboard() - }, 1200) - webView.postDelayed({ - injectDownloadHandler(webView) - }, 2000) + webView.requestFocus() + showKeyboard() + }, 1000) } } diff --git a/app/src/main/res/drawable/ic_attach.xml b/app/src/main/res/drawable/ic_attach.xml deleted file mode 100644 index 7c9d932..0000000 --- a/app/src/main/res/drawable/ic_attach.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 5859fe5..b7bb7df 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -5,18 +5,13 @@ android:viewportWidth="108" android:viewportHeight="108"> + android:fillColor="#FFFFFF" + android:pathData="M54,30 L54,78 M30,54 L78,54" + android:strokeWidth="8" + android:strokeColor="#FFFFFF"/> - - - + android:pathData="M54,40 A14,14 0 1,1 54,68 A14,14 0 1,1 54,40" + android:strokeWidth="4" + android:strokeColor="#FFFFFF"/> \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml deleted file mode 100644 index b9c1557..0000000 --- a/app/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c197d89..9b5e432 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,4 +1,4 @@ - #1A1A2E + #6750A4 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 253b724..aba6527 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,8 +9,4 @@ Yes No Chat history cleared - Attach file - File attached - No app found to open this file - Storage permission required \ No newline at end of file