Compare commits
No commits in common. "6b940730f43206f199c775d10d6af1bb591bc40f" and "31ea334898aff04b0b2b451cadc4aedb913ba01b" have entirely different histories.
6b940730f4
...
31ea334898
8 changed files with 23 additions and 372 deletions
|
|
@ -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'
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
|
|
|||
|
|
@ -1,56 +1,24 @@
|
|||
package com.duckai.app.web
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.ValueCallback
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.webkit.DownloadListener
|
||||
import android.widget.Toast
|
||||
import android.os.Environment
|
||||
import android.app.DownloadManager
|
||||
import android.provider.Settings
|
||||
import java.io.File as IoFile
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.duckai.app.R
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var webView: WebView
|
||||
private var pendingFileCallback: ValueCallback<Array<Uri>>? = 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
|
||||
|
|
@ -78,264 +45,18 @@ 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<Array<Uri>>?,
|
||||
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({
|
||||
showKeyboard()
|
||||
}, 500)
|
||||
}, 800)
|
||||
}
|
||||
|
||||
private fun showKeyboard() {
|
||||
|
|
@ -382,17 +89,6 @@ class MainActivity : AppCompatActivity() {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M16.5,6v11.5c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4V5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5v10.5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V6H10v9.5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5V5c0,-2.21 -1.79,-4 -4,-4S7,2.79 7,5v12.5c0,3.04 2.46,5.5 5.5,5.5s5.5,-2.46 5.5,-5.5V6h-1.5z"/>
|
||||
</vector>
|
||||
|
|
@ -5,18 +5,13 @@
|
|||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#DE5833"
|
||||
android:pathData="M54,0C24.18,0,0,24.18,0,54s24.18,54,54,54s54-24.18,54-54S83.82,0,54,0z"/>
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M54,30 L54,78 M30,54 L78,54"
|
||||
android:strokeWidth="8"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M54,27c-14.91,0-27,12.09-27,27s12.09,27,27,27s27-12.09,27-27S68.91,27,54,27z"/>
|
||||
<path
|
||||
android:fillColor="#1A1A2E"
|
||||
android:pathData="M54,35c-10.55,0-19,8.45-19,19s8.45,19,19,19s19-8.45,19-19S64.55,35,54,35z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M48,42c0,3.31-2.69,6-6,6s-6-2.69-6-6s2.69-6,6-6S48,38.69,48,42z"/>
|
||||
<path
|
||||
android:fillColor="#FF9F1C"
|
||||
android:pathData="M45,51l18,5l-18,5z"/>
|
||||
android:pathData="M54,40 A14,14 0 1,1 54,68 A14,14 0 1,1 54,40"
|
||||
android:strokeWidth="4"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.DuckAI" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">#D0BCFF</item>
|
||||
<item name="colorOnPrimary">#381E72</item>
|
||||
<item name="colorPrimaryContainer">#4F378B</item>
|
||||
<item name="colorOnPrimaryContainer">#EADDFF</item>
|
||||
<item name="colorSecondary">#CCC2DC</item>
|
||||
<item name="colorOnSecondary">#332D41</item>
|
||||
<item name="colorSecondaryContainer">#4A4458</item>
|
||||
<item name="colorOnSecondaryContainer">#E8DEF8</item>
|
||||
<item name="colorTertiary">#EFB8C8</item>
|
||||
<item name="colorOnTertiary">#492532</item>
|
||||
<item name="colorTertiaryContainer">#633B48</item>
|
||||
<item name="colorOnTertiaryContainer">#FFD8E4</item>
|
||||
<item name="colorError">#F2B8B5</item>
|
||||
<item name="colorOnError">#601410</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#1A1A2E</color>
|
||||
<color name="ic_launcher_background">#6750A4</color>
|
||||
</resources>
|
||||
|
|
@ -9,8 +9,4 @@
|
|||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="history_cleared">Chat history cleared</string>
|
||||
<string name="attach_file">Attach file</string>
|
||||
<string name="file_attached">File attached</string>
|
||||
<string name="no_app_for_file">No app found to open this file</string>
|
||||
<string name="permission_required">Storage permission required</string>
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue