Compare commits
10 commits
31ea334898
...
6b940730f4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b940730f4 | ||
|
|
1b409ea9a7 | ||
|
|
7349e4ef0c | ||
|
|
ffb1b8dc0d | ||
|
|
541b5c0672 | ||
|
|
1558ead809 | ||
|
|
00c5db0253 | ||
|
|
8637603930 | ||
|
|
fbd68b1c1c | ||
|
|
db4f977982 |
8 changed files with 372 additions and 23 deletions
|
|
@ -9,7 +9,7 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.duckai.app"
|
applicationId "com.duckai.app"
|
||||||
minSdk 31
|
minSdk 33
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
|
|
@ -35,5 +35,6 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation 'androidx.activity:activity-ktx:1.8.2'
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,56 @@
|
||||||
package com.duckai.app.web
|
package com.duckai.app.web
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.ValueCallback
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
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.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.duckai.app.R
|
import com.duckai.app.R
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var webView: WebView
|
private lateinit var webView: WebView
|
||||||
|
private var pendingFileCallback: ValueCallback<Array<Uri>>? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val BASE_URL = "https://duck.ai"
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
@ -37,6 +69,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
setSupportZoom(false)
|
setSupportZoom(false)
|
||||||
loadWithOverviewMode = true
|
loadWithOverviewMode = true
|
||||||
useWideViewPort = true
|
useWideViewPort = true
|
||||||
|
setAllowFileAccessFromFileURLs(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
webView.isFocusable = true
|
webView.isFocusable = true
|
||||||
|
|
@ -45,18 +78,264 @@ class MainActivity : AppCompatActivity() {
|
||||||
CookieManager.getInstance().setAcceptCookie(true)
|
CookieManager.getInstance().setAcceptCookie(true)
|
||||||
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, 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() {
|
webView.webViewClient = object : WebViewClient() {
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
view?.postDelayed({
|
view?.postDelayed({ tryFocusInput(view) }, 2000)
|
||||||
view.evaluateJavascript(
|
view?.postDelayed({ injectDownloadHandler(view) }, 1500)
|
||||||
"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() {
|
private fun loadUrlFromIntent() {
|
||||||
|
|
@ -70,9 +349,23 @@ class MainActivity : AppCompatActivity() {
|
||||||
webView.loadUrl(url)
|
webView.loadUrl(url)
|
||||||
|
|
||||||
window.decorView.postDelayed({
|
window.decorView.postDelayed({
|
||||||
|
requestFocusAndShowKeyboard()
|
||||||
|
}, 1200)
|
||||||
|
webView.postDelayed({
|
||||||
|
injectDownloadHandler(webView)
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestFocusAndShowKeyboard() {
|
||||||
webView.requestFocus()
|
webView.requestFocus()
|
||||||
|
|
||||||
|
// Try to focus input field
|
||||||
|
tryFocusInput(webView)
|
||||||
|
|
||||||
|
// Show keyboard after JS
|
||||||
|
window.decorView.postDelayed({
|
||||||
showKeyboard()
|
showKeyboard()
|
||||||
}, 800)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showKeyboard() {
|
private fun showKeyboard() {
|
||||||
|
|
@ -86,9 +379,20 @@ class MainActivity : AppCompatActivity() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
webView.onResume()
|
webView.onResume()
|
||||||
super.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?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
|
@ -100,9 +404,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
val url = "$BASE_URL/?q=${Uri.encode(query)}"
|
val url = "$BASE_URL/?q=${Uri.encode(query)}"
|
||||||
webView.loadUrl(url)
|
webView.loadUrl(url)
|
||||||
webView.postDelayed({
|
webView.postDelayed({
|
||||||
webView.requestFocus()
|
requestFocusAndShowKeyboard()
|
||||||
showKeyboard()
|
}, 1200)
|
||||||
}, 1000)
|
webView.postDelayed({
|
||||||
|
injectDownloadHandler(webView)
|
||||||
|
}, 2000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
10
app/src/main/res/drawable/ic_attach.xml
Normal file
10
app/src/main/res/drawable/ic_attach.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?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,13 +5,18 @@
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#DE5833"
|
||||||
android:pathData="M54,30 L54,78 M30,54 L78,54"
|
android:pathData="M54,0C24.18,0,0,24.18,0,54s24.18,54,54,54s54-24.18,54-54S83.82,0,54,0z"/>
|
||||||
android:strokeWidth="8"
|
|
||||||
android:strokeColor="#FFFFFF"/>
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M54,40 A14,14 0 1,1 54,68 A14,14 0 1,1 54,40"
|
android:pathData="M54,27c-14.91,0-27,12.09-27,27s12.09,27,27,27s27-12.09,27-27S68.91,27,54,27z"/>
|
||||||
android:strokeWidth="4"
|
<path
|
||||||
android:strokeColor="#FFFFFF"/>
|
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"/>
|
||||||
</vector>
|
</vector>
|
||||||
21
app/src/main/res/values-night/themes.xml
Normal file
21
app/src/main/res/values-night/themes.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="ic_launcher_background">#6750A4</color>
|
<color name="ic_launcher_background">#1A1A2E</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -9,4 +9,8 @@
|
||||||
<string name="yes">Yes</string>
|
<string name="yes">Yes</string>
|
||||||
<string name="no">No</string>
|
<string name="no">No</string>
|
||||||
<string name="history_cleared">Chat history cleared</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>
|
</resources>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue