├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── ic_launcher-playstore.png
│ │ ├── res
│ │ │ ├── drawable
│ │ │ │ ├── deepl_logo.png
│ │ │ │ ├── deepl_mini_logo.png
│ │ │ │ ├── settings_black_24dp.png
│ │ │ │ ├── splash.xml
│ │ │ │ ├── ic_baseline_refresh_32.xml
│ │ │ │ ├── ic_deepl_mini_logo.xml
│ │ │ │ ├── baseline_settings_24.xml
│ │ │ │ ├── app_logo.xml
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ └── ic_launcher_monochrome.xml
│ │ │ ├── drawable-night
│ │ │ │ ├── deepl_logo.png
│ │ │ │ └── deepl_mini_logo.png
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── values-v21
│ │ │ │ └── themes.xml
│ │ │ ├── values-v29
│ │ │ │ └── arrays.xml
│ │ │ ├── values-night
│ │ │ │ └── colors.xml
│ │ │ ├── values-nl
│ │ │ │ └── strings.xml
│ │ │ ├── values-es
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher_round.xml
│ │ │ │ └── ic_launcher.xml
│ │ │ ├── drawable-v23
│ │ │ │ └── splash.xml
│ │ │ ├── xml
│ │ │ │ ├── network_security_config.xml
│ │ │ │ └── root_preferences.xml
│ │ │ ├── xml-v25
│ │ │ │ └── shortcuts.xml
│ │ │ ├── values
│ │ │ │ ├── colors.xml
│ │ │ │ ├── strings_not_translatable.xml
│ │ │ │ ├── arrays.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── themes.xml
│ │ │ ├── raw
│ │ │ │ ├── isrg_root_x2.pem
│ │ │ │ └── isrg_root_x1.pem
│ │ │ ├── layout
│ │ │ │ ├── settings_activity.xml
│ │ │ │ ├── popup_layout.xml
│ │ │ │ ├── network_err.xml
│ │ │ │ └── activity_main.xml
│ │ │ ├── values-ja
│ │ │ │ └── strings.xml
│ │ │ ├── values-zh
│ │ │ │ └── strings.xml
│ │ │ ├── values-tr
│ │ │ │ └── strings.xml
│ │ │ ├── values-de
│ │ │ │ └── strings.xml
│ │ │ ├── values-ro
│ │ │ │ └── strings.xml
│ │ │ ├── values-ru
│ │ │ │ └── strings.xml
│ │ │ ├── values-it
│ │ │ │ └── strings.xml
│ │ │ ├── values-fr
│ │ │ │ └── strings.xml
│ │ │ └── values-cs
│ │ │ │ └── strings.xml
│ │ ├── assets
│ │ │ ├── debug.js
│ │ │ ├── patch-darkThemeFix.js
│ │ │ ├── legacy
│ │ │ │ ├── patch-clipboard.js
│ │ │ │ ├── switchLanguage.html
│ │ │ │ └── patch-switchLanguage.js
│ │ │ ├── DeepL_Logo_lightBlue_v2.svg
│ │ │ ├── hide-elements.js
│ │ │ └── DeepL_Text_light.svg
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── deeplviewer
│ │ │ │ ├── service
│ │ │ │ └── QSTileService.kt
│ │ │ │ ├── webview
│ │ │ │ ├── WebAppInterface.kt
│ │ │ │ ├── MyWebViewClient.kt
│ │ │ │ └── NestedScrollWebView.kt
│ │ │ │ ├── helper
│ │ │ │ ├── UrlHelper.kt
│ │ │ │ └── CookieManagerHelper.kt
│ │ │ │ ├── config
│ │ │ │ └── WebViewConfig.kt
│ │ │ │ ├── App.kt
│ │ │ │ └── activity
│ │ │ │ ├── SettingsActivity.kt
│ │ │ │ ├── FloatingTextSelection.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── androidTest
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── deeplviewer
│ │ │ └── WebViewScreenshotTest.kt
│ └── test
│ │ └── java
│ │ └── com
│ │ └── example
│ │ └── deeplviewer
│ │ └── ExampleUnitTest.kt
├── screenshots
│ └── debug
│ │ └── unknown_webview_top_screen.png
├── proguard-rules.pro
└── build.gradle.kts
├── fastlane
├── metadata
│ └── android
│ │ ├── ja
│ │ ├── short_description.txt
│ │ └── full_description.txt
│ │ ├── en-US
│ │ ├── short_description.txt
│ │ ├── images
│ │ │ ├── icon.png
│ │ │ └── phoneScreenshots
│ │ │ │ ├── 1.png
│ │ │ │ ├── 2.png
│ │ │ │ ├── 3.png
│ │ │ │ └── 4.png
│ │ └── full_description.txt
│ │ ├── cs
│ │ ├── short_description.txt
│ │ └── full_description.txt
│ │ ├── de
│ │ ├── short_description.txt
│ │ └── full_description.txt
│ │ ├── tr
│ │ ├── short_description.txt
│ │ └── full_description.txt
│ │ ├── it
│ │ ├── short_description.txt
│ │ └── full_description.txt
│ │ ├── es
│ │ ├── short_description.txt
│ │ └── full_description.txt
│ │ ├── ru
│ │ ├── short_description.txt
│ │ └── full_description.txt
│ │ └── fr
│ │ ├── short_description.txt
│ │ └── full_description.txt
├── Appfile
└── Fastfile
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── Gemfile
├── .gitignore
├── settings.gradle.kts
├── gradle.properties
├── LICENSE
├── .github
└── workflows
│ ├── ci_build.yaml
│ ├── capture_webview_screenshots.yml
│ └── screenshot_diff_check.yml
├── README_ZH.md
├── README_JA.md
├── README_RU.md
├── README.md
├── README_TR.md
├── README_CS.md
├── README_IT.md
├── README_DE.md
├── README_FR.md
├── gradlew.bat
├── Gemfile.lock
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/fastlane/metadata/android/ja/short_description.txt:
--------------------------------------------------------------------------------
1 | DeepL翻訳の非公式Androidアプリ
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Unofficial Android App for DeepL Translator
--------------------------------------------------------------------------------
/fastlane/metadata/android/cs/short_description.txt:
--------------------------------------------------------------------------------
1 | Neoficiální Android aplikace pro DeepL Překladač
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/de/short_description.txt:
--------------------------------------------------------------------------------
1 | inoffizielle Android App für den Deepl Translator
--------------------------------------------------------------------------------
/fastlane/metadata/android/tr/short_description.txt:
--------------------------------------------------------------------------------
1 | DeepL Çeviri için resmi olmayan Android uygulaması
--------------------------------------------------------------------------------
/fastlane/metadata/android/it/short_description.txt:
--------------------------------------------------------------------------------
1 | Un'applicazione Android non ufficiale per traduzioni DeepL
--------------------------------------------------------------------------------
/fastlane/metadata/android/es/short_description.txt:
--------------------------------------------------------------------------------
1 | Aplicación Android no oficial para for DeepL Translator
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ru/short_description.txt:
--------------------------------------------------------------------------------
1 | Неофициальное Android-приложение для DeepL переводчика
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr/short_description.txt:
--------------------------------------------------------------------------------
1 | Application Android non officielle pour le traducteur DeepL
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/deepl_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/drawable/deepl_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/deepl_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/drawable-night/deepl_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/deepl_mini_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/drawable/deepl_mini_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/deepl_mini_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/drawable-night/deepl_mini_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/settings_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/drawable/settings_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/screenshots/debug/unknown_webview_top_screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/screenshots/debug/unknown_webview_top_screen.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakusaku3939/DeepLAndroid/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6 |
7 | # gem "rails"
8 | gem "fastlane"
--------------------------------------------------------------------------------
/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
2 | package_name("com.example.deeplviewer") # e.g. com.krausefx.app
3 |
--------------------------------------------------------------------------------
/app/src/main/assets/debug.js:
--------------------------------------------------------------------------------
1 | document.body.addEventListener('click', (e) => {
2 | console.log("class: " + e.target.className);
3 | console.log("id: " + e.target.id);
4 | /*console.log("html: " + e.target.outerHTML);*/
5 | });
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v29/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | - @string/theme_light
4 | - @string/theme_dark
5 | - @string/theme_default
6 |
7 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ja/full_description.txt:
--------------------------------------------------------------------------------
1 | DeepL翻訳の非公式Androidアプリです。AndroidのGUIコンポーネントの「WebView」といわれる技術を使用してDeepL翻訳をネイティブアプリ風にしたものです。必要な要素以外はJavaScriptを使用して隠しています。
2 |
3 | 機能一覧
4 |
5 | * 翻訳画面のみをWebViewで表示
6 | * ダークモードへの切り替え
7 | * 選択メニューからDeepLアプリを起動
8 | * アプリアイコンを長押しで設定画面に入る
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @color/background_material_dark
4 | #ffffff
5 | #ffffff
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | /.kotlin
15 | .cxx
16 | .idea
--------------------------------------------------------------------------------
/app/src/main/res/values-nl/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DeepL
3 | Afbeelding
4 | Tekst is gekopieerd
5 | Oeps, geen verbinding met DeepL server
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values-es/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DeepL
3 | Imagen
4 | El texto fue copiado
5 | Vaya, hubo problemas al conectar con los servidores de DeepL
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/assets/patch-darkThemeFix.js:
--------------------------------------------------------------------------------
1 | $('.dl_header_menu_v2__logo__img').attr('src', 'data:image/svg+xml;base64,' + Android.stringToBase64String(Android.getAssetsText('DeepL_Logo_lightBlue_v2.svg')));
2 | $('.dl_logo_text').attr('src', 'data:image/svg+xml;base64,' + Android.stringToBase64String(Android.getAssetsText('DeepL_Text_light.svg')));
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionSha256Sum=845952a9d6afa783db70bb3b0effaae45ae5542ca2bb7929619e8af49cb634cf
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/app/src/androidTest/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/assets/legacy/patch-clipboard.js:
--------------------------------------------------------------------------------
1 | var copyButton = $('.Target--Dwohg');
2 |
3 | if (copyButton.length > 1) {
4 | copyButton.last().off('click');
5 | copyButton.last().on('click', function () {
6 | const text = $('[class^="TextInput-module--textarea--"]').last().val();
7 | /* Discontinued because the copy button now works on WebView as well
8 | Android.copyClipboard(text);
9 | */
10 | });
11 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/deeplviewer/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.deeplviewer
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/fastlane/metadata/android/ru/full_description.txt:
--------------------------------------------------------------------------------
1 | Это неофициальное Android-приложение для DeepL переводчика. Оно использует технологию WebView, компонент интерфейса Android, чтобы выглядеть как нативное приложение. Чтобы скрыть все ненужные элементы, используется JavaScript.
2 |
3 | Функции:
4 |
5 | * Показывать только экран перевода через WebView
6 | * Переключение в темный режим
7 | * Запустить DeepL из меню выделенного текста
8 | * Откройте экран настроек, нажав и удерживая значок приложения
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | This is an unofficial Android app for DeepL translation. It uses a technology called WebView, an Android GUI component, to make it look like a native app. JavaScript is used to hide all but the most necessary elements.
2 |
3 | Features:
4 |
5 | * Display only the translation screen by WebView
6 | * Switching to dark mode
7 | * Launch DeepL from floating text selection action
8 | * Long press on the application icon to access the settings screen
--------------------------------------------------------------------------------
/fastlane/metadata/android/tr/full_description.txt:
--------------------------------------------------------------------------------
1 | DeepL çeviri için resmi olmayan bir Android uygulamasıdır. Yerel bir uygulama gibi görünmesini sağlamak için bir Android Arayüz bileşeni olan WebView adlı bir teknoloji kullanır. JavaScript gereksiz öğeleri gizlemek için kullanılır.
2 |
3 |
4 | Özellikler:
5 |
6 | * WebView ile yalnızca çeviri ekranını görüntüleme
7 | * Koyu moda geçme
8 | * DeepL'i kayan metin seçimi eyleminden başlatma
9 | * Uygulama simgesine basılı tutarak ayarlar ekranına erişin
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v23/splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/cs/full_description.txt:
--------------------------------------------------------------------------------
1 | Toto je neoficiální Android aplikace pro DeepL překlady. Využívá technologii WebView, komponentu grafického uživatelského rozhraní systému Android, aby vypadala jako nativní aplikace. JavaScript se používá ke skrytí všech prvků kromě těch nejnutnějších.
2 |
3 | Funkce:
4 |
5 | * Zobrazení pouze obrazovky překladu pomocí WebView
6 | * Přepínání do tmavého režimu
7 | * Spuštění DeepL z akce výběru v plovoucím textu
8 | * Dlouhý stisk ikony aplikace umožňuje otevřít obrazovku s nastavením
9 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/it/full_description.txt:
--------------------------------------------------------------------------------
1 | Questa è un'applicazione Android non ufficiale per traduzioni DeepL. Usa una tecnologia chiamata WebView, una componente GUI di Android, per farla sembrare come un'app nativa. JavaScript è usato per nascondere tutto tranne gli elementi più necessari.
2 |
3 | Funzionalità:
4 |
5 | * Mostra solo la schermata di traduzione tramite WebView.
6 | * Cambiare in modalità scura.
7 | * Lancia DeepL dall'azione di selezione del testo.
8 | * Accedere alla schermata delle impostazioni tenendo premuta l'icona dell'applicazione
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_refresh_32.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/es/full_description.txt:
--------------------------------------------------------------------------------
1 | Esta es una aplicación no oficial de Android para el traductor DeepL. Utiliza una tecnología llamada WebView, un componente GUI de Android, para que parezca una aplicación nativa. Se utiliza JavaScript para ocultar todos los elementos excepto los más necesarios.
2 |
3 | Características:
4 |
5 | * Muestra sólo la pantalla de traducción mediante WebView
6 | * Cambiar al modo oscuro
7 | * Lanza DeepL desde la acción de selección de texto flotante
8 | * Aceda ao ecrã de definições premindo e mantendo premido o ícone da aplicação
9 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/de/full_description.txt:
--------------------------------------------------------------------------------
1 | Dies ist eine inoffizielle Android-App für den DeepL-Übersetzer. Es verwendet eine Technologie namens WebView, eine Android-GUI-Komponente, um es wie eine native App aussehen zu lassen. JavaScript wird verwendet, um alle bis auf die notwendigsten Elemente auszublenden.
2 |
3 | Funktionen:
4 |
5 | * Den Übersetzungsbildschirm im WebView anzeigen lassen
6 | * In den Dunkelmodus wechseln
7 | * Starten Sie DeepL über die schwebende Textauswahlaktion
8 | * Rufen Sie den Einstellungsbildschirm auf, indem Sie das Anwendungssymbol drücken und gedrückt halten
--------------------------------------------------------------------------------
/app/src/main/res/xml-v25/shortcuts.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr/full_description.txt:
--------------------------------------------------------------------------------
1 | Il s'agit d'une application Android non officielle pour le traducteur DeepL. Elle utilise une technologie appelée WebView, un composant d'interface graphique d'Android, afin de la faire ressembler à une application native. JavaScript est utilisé pour masquer tous les éléments à l'exception des plus nécessaires.
2 |
3 | Fonctionnalités:
4 |
5 | * Afficher uniquement l'écran de traduction via WebView
6 | * Passage en mode sombre
7 | * Lancer DeepL par une action du menu flottant de sélection de texte
8 | * Accédez à l'écran des paramètres en appuyant sur l'icône de l'application et en la maintenant enfoncée
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 |
8 | resolutionStrategy{
9 | eachPlugin{
10 | when(requested.id.id){
11 | "shot" -> {
12 | useModule("com.karumi:shot:${requested.version}")
13 | }
14 | }
15 | }
16 | }
17 | }
18 |
19 | dependencyResolutionManagement {
20 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
21 | repositories {
22 | google()
23 | mavenCentral()
24 | }
25 | }
26 |
27 | rootProject.name = "DeepLViewer"
28 | include(":app")
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #042C49
4 | #ffffff
5 | #00000000
6 |
7 | #204060
8 | #204060
9 | #204060
10 | @color/background_material_light
11 | @color/background
12 | @color/background
13 |
14 | @color/colorAccent
15 | #212121
16 |
--------------------------------------------------------------------------------
/app/src/main/assets/legacy/switchLanguage.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings_not_translatable.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | defaultDarkMode
5 | isEnabledPopupMode
6 | isEnabledTranslateButton
7 | isEnabledAutoDeleteCookies
8 | appVersion
9 | https://github.com/sakusaku3939/DeepLAndroid/releases
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | - @string/theme_light
4 | - @string/theme_dark
5 | - @string/theme_battery
6 |
7 |
8 |
9 | - @string/theme_AppCompatDelegate_MODE_NIGHT_NO
10 | - @string/theme_AppCompatDelegate_MODE_NIGHT_YES
11 | - @string/theme_AppCompatDelegate_MODE_NIGHT_FOLLOW_SYSTEM
12 |
13 |
14 | 1
15 | 2
16 | -1
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/raw/isrg_root_x2.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
3 | CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
4 | R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
5 | MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
6 | ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
7 | EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
8 | +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
9 | ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
10 | AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
11 | zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
12 | tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
13 | /q4AaOeMSQ+2b1tbFfLn
14 | -----END CERTIFICATE-----
15 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/settings_activity.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
13 |
14 |
19 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -dontwarn com.google.errorprone.annotations.InlineMe
24 | -dontwarn com.google.errorprone.annotations.MustBeClosed
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## For more details on how to configure your build environment visit
2 | # http://www.gradle.org/docs/current/userguide/build_environment.html
3 | #
4 | # Specifies the JVM arguments used for the daemon process.
5 | # The setting is particularly useful for tweaking memory settings.
6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
7 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
8 | #
9 | # When configured, Gradle will run in incubating parallel mode.
10 | # This option should only be used with decoupled projects. More details, visit
11 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
12 | # org.gradle.parallel=true
13 | #Mon Oct 18 15:35:31 JST 2021
14 | kotlin.code.style=official
15 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options=-Xmx2048M
16 | android.useAndroidX=true
17 | android.enableJetifier=false
18 | android.enableR8.fullMode=true
19 |
--------------------------------------------------------------------------------
/app/src/main/assets/legacy/patch-switchLanguage.js:
--------------------------------------------------------------------------------
1 | /* Deactivated as it is displayed by default due to an update to the DeepL server */
2 | if (!$('#lang_switch').length) {
3 | $('html > head').append($(''));
4 | $(Android.getAssetsText('switchLanguage.html')).click(function () {
5 | this.classList.contains("switched") ? this.classList.remove("switched") : this.classList.add("switched");
6 | window.location = window.location.href.split('#')[0] +
7 | '#' + document.getElementsByClassName('lmt__language_select lmt__language_select--target')[0].getAttribute('dl-selected-lang').split('-')[0] +
8 | '/' + document.getElementsByClassName('lmt__language_select lmt__language_select--source')[0].getAttribute('dl-selected-lang').split('-')[0] +
9 | '/' + encodeURI(document.getElementsByClassName('lmt__textarea lmt__target_textarea lmt__textarea_base_style')[0].value.replace("/", "\\/"));
10 | }).prependTo($('.lmt__language_container')[1]);
11 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-ja/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DeepL翻訳
3 | 画像
4 | テキストをコピーしました
5 | DeepLサーバーへの接続に失敗しました
6 | 設定
7 | テーマの設定
8 | テーマ
9 | ライト
10 | ダーク
11 | バッテリーセーバーによる設定
12 | システムのデフォルト
13 | 選択メニューのオプション
14 | ポップアップモードを使用
15 | 選択メニューにDeepL翻訳ボタンを表示する
16 | アプリのオプション
17 | 終了時にCookieを自動的に削除する
18 | このアプリについて
19 | バージョン
20 |
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # This file contains the fastlane.tools configuration
2 | # You can find the documentation at https://docs.fastlane.tools
3 | #
4 | # For a list of all available actions, check out
5 | #
6 | # https://docs.fastlane.tools/actions
7 | #
8 | # For a list of all available plugins, check out
9 | #
10 | # https://docs.fastlane.tools/plugins/available-plugins
11 | #
12 |
13 | # Uncomment the line if you want fastlane to automatically update itself
14 | # update_fastlane
15 |
16 | default_platform(:android)
17 |
18 | platform :android do
19 | desc "Runs all the tests"
20 | lane :test do
21 | gradle(task: "test")
22 | end
23 |
24 | desc "Submit a new Beta Build to Crashlytics Beta"
25 | lane :beta do
26 | gradle(task: "clean assembleRelease")
27 | crashlytics
28 |
29 | # sh "your_script.sh"
30 | # You can also use other beta testing services here
31 | end
32 |
33 | desc "Deploy a new version to the Google Play"
34 | lane :deploy do
35 | gradle(task: "clean assembleRelease")
36 | upload_to_play_store
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/deeplviewer/service/QSTileService.kt:
--------------------------------------------------------------------------------
1 | package com.example.deeplviewer.service
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.PendingIntent
5 | import android.content.Intent
6 | import android.os.Build
7 | import android.service.quicksettings.TileService
8 | import androidx.annotation.RequiresApi
9 | import com.example.deeplviewer.activity.FloatingTextSelection
10 |
11 | @RequiresApi(Build.VERSION_CODES.N)
12 | class QSTileService : TileService() {
13 | override fun onClick() {
14 | val intent = Intent(applicationContext, FloatingTextSelection::class.java)
15 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
16 | intent.putExtra(Intent.EXTRA_TEXT, "")
17 |
18 | @SuppressLint("StartActivityAndCollapseDeprecated")
19 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
20 | startActivityAndCollapse(
21 | PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
22 | )
23 | } else {
24 | startActivityAndCollapse(intent)
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Aoki Yuki
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | DeepL
4 | 图片
5 | 文本已复制
6 | 哎呀,连接 DeepL 服务失败
7 | 设置
8 | 主题设置
9 | 主题
10 | 浅色
11 | 深色
12 | 由省电模式设置
13 | 系统默认
14 | 菜单选择
15 | 使用弹窗模式
16 | 在文本选择菜单中显示 DeepL 按钮
17 | 应用选项
18 | 退出应用时自动删除 cookie
19 | 关于此应用
20 | 版本
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_deepl_mini_logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values-tr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DeepL
3 | Resim
4 | Metin kopyalandı
5 | Eyvah! DeepL sunucusuna bağlanılamadı
6 | Ayarlar
7 | Tema ayarları
8 | Tema
9 | Açık
10 | Koyu
11 | Pil tasarrufuna göre ayarla
12 | Sistem varsayılanı
13 | Seçim menüsü seçenekleri
14 | Açılır pencere modunu kullan
15 | Metin seçim menüsünde DeepL tuşunu göster
16 | Uygulama seçenekleri
17 | Uygulamadan çıkarken çerezleri otomatik olarak sil
18 | Bu uygulama hakkında
19 | Sürüm
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_settings_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DeepL
3 | Bild
4 | Text wurde kopiert
5 | Hoppla, Verbindung zum DeepL-Server konnte nicht hergestellt werden
6 | Einstellungen
7 | Design-Einstellungen
8 | Design
9 | Hell
10 | Dunkel
11 | Durch Batteriesparmodus eingestellt
12 | Systemstandard
13 | Optionen des Auswahlmenüs
14 | Popup-Modus verwenden
15 | DeepL-Schaltfläche im Textauswahlmenü anzeigen
16 | App-Optionen
17 | Cookies beim Beenden der App automatisch löschen
18 | Über diese App
19 | Version
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ro/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DeepL
3 | Imagine
4 | Textul a fost copiat
5 | Hopa, nu s-a putut conecta la serverul DeepL
6 | Setări
7 | Setări temă
8 | Temă
9 | Luminoasă
10 | Întunecată
11 | Setat de economisirea bateriei
12 | Implicit sistem
13 | Opțiuni meniu de selecție
14 | Utilizați modul pop-up
15 | Afișați butonul DeepL în meniul de selecție a textului
16 | Opțiuni aplicație
17 | Ștergeți automat cookie-urile la ieșirea din aplicație
18 | Despre această aplicație
19 | Versiune
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DeepL
3 | Изображение
4 | Текст скопирован в буфер обмена
5 | Не удалось подключиться к серверам DeepL
6 | Настройки
7 | Настройки темы
8 | Тема
9 | Светлая
10 | Тёмная
11 | Установленная режимом энергосбережения
12 | Как в системе
13 | Параметры меню выбора
14 | Использовать режим всплывающего окна
15 | Показывать кнопку DeepL в меню выбора текста
16 | Параметры приложения
17 | Автоматически удалять куки при выходе из приложения
18 | О программе
19 | Версия
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/popup_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/values-it/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DeepL
3 | Immagine
4 | Il Testo è stato Copiato
5 | Oops, errore di connessione con il server DeepL
6 | Impostazioni
7 | Impostazioni tema
8 | Tema
9 | Chiaro
10 | Scuro
11 | Impostato da Risparmio Batteria
12 | Predefinito di Sistema
13 | Opzioni del menu di selezione
14 | Usa modalità pop-up
15 | Mostra il pulsante DeepL nel menù di selezione del testo
16 | Opzioni dell\'app
17 | Elimina automaticamente i cookie alla chiusura dell\'app
18 | Informazioni app
19 | Versione
20 |
21 |
--------------------------------------------------------------------------------
/.github/workflows/ci_build.yaml:
--------------------------------------------------------------------------------
1 | name: APK Build
2 | on:
3 | push:
4 | branches:
5 | - master
6 |
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout the code
15 | uses: actions/checkout@v4
16 |
17 | - name: Set up JDK 17
18 | uses: actions/setup-java@v4
19 | with:
20 | distribution: 'temurin'
21 | java-version: '17'
22 |
23 | - name: Set up Gradle
24 | uses: gradle/actions/setup-gradle@v4
25 |
26 | - name: Cache Gradle packages
27 | uses: actions/cache@v3
28 | with:
29 | path: |
30 | ~/.gradle/caches
31 | ~/.gradle/wrapper
32 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
33 | restore-keys: |
34 | ${{ runner.os }}-gradle-
35 |
36 | - name: Build the app
37 | run: |
38 | chmod +x gradlew
39 | ./gradlew build --stacktrace
40 |
41 | - name: Upload APK
42 | uses: actions/upload-artifact@v4
43 | with:
44 | name: debug-apk
45 | path: app/build/outputs/apk/debug/*.apk
46 | if-no-files-found: error
47 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/deeplviewer/webview/WebAppInterface.kt:
--------------------------------------------------------------------------------
1 | package com.example.deeplviewer.webview
2 |
3 | import android.content.ClipData
4 | import android.content.ClipboardManager
5 | import android.content.Context
6 | import android.util.Base64
7 | import android.webkit.JavascriptInterface
8 | import android.widget.Toast
9 | import androidx.annotation.Keep
10 | import com.example.deeplviewer.R
11 |
12 | @Keep
13 | class WebAppInterface(private val context: Context) {
14 | @JavascriptInterface
15 | fun copyClipboard(text: String) {
16 | val clipboard: ClipboardManager =
17 | context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
18 | val clip: ClipData = ClipData.newPlainText("translation_text", text)
19 | clipboard.setPrimaryClip(clip)
20 | Toast.makeText(context, context.getString(R.string.copy_clipboard), Toast.LENGTH_SHORT)
21 | .show()
22 | }
23 |
24 | @JavascriptInterface
25 | fun stringToBase64String(s: String): String {
26 | return Base64.encodeToString(s.toByteArray(), Base64.DEFAULT)
27 | }
28 |
29 | @JavascriptInterface
30 | fun getAssetsText(fileName: String): String {
31 | return context.assets.open(fileName).reader(Charsets.UTF_8).use { it.readText() }
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-fr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DeepL
3 | Image
4 | Le texte a été copié
5 | Oups, échec de connexion au serveur de DeepL
6 | Paramètres
7 | Paramètres du thème
8 | Thème
9 | Clair
10 | Obscur
11 | Défini par l\'économiseur de batterie
12 | Identique au système
13 | Options de menu de sélection
14 | Utiliser le mode fenêtré
15 | Afficher le bouton DeepL dans le menu de sélection de texte
16 | Options de l\'application
17 | Supprimer automatiquement les cookies à la fermeture de l\'application
18 | À propos de cette app
19 | Version
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/deeplviewer/helper/UrlHelper.kt:
--------------------------------------------------------------------------------
1 | package com.example.deeplviewer.helper
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 |
6 | object UrlHelper {
7 | /**
8 | * Builds the start URL for the DeepL translator based on the provided parameters
9 | */
10 | fun buildStartUrl(
11 | context: Context,
12 | originUrl: String = "https://www.deepl.com/",
13 | pageType: String = "translator",
14 | urlParam: String = "#en/en/"
15 | ): String {
16 | val configPrefs = context.getSharedPreferences("config", Context.MODE_PRIVATE)
17 | val buildPageType = configPrefs.getString("pageType", pageType)
18 | ?: pageType
19 | val buildUrlParam = configPrefs.getString(
20 | "urlParam",
21 | urlParam
22 | ) ?: urlParam
23 |
24 | return "$originUrl$buildPageType$buildUrlParam"
25 | }
26 |
27 | /**
28 | * Builds a URL for the WebView based on the provided start URL and text
29 | */
30 | fun buildWebViewUrl(startUrl: String, text: String): String {
31 | if (text.isEmpty()) return startUrl
32 |
33 | val processedText = text.replace("/", "\\/")
34 | val encodedText = Uri.encode(processedText)
35 | return "$startUrl$encodedText"
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | DeepL
4 | Image
5 | Text has been Copied
6 | Oops, failed to connect to DeepL server
7 | Settings
8 | Theme settings
9 | Theme
10 | Light
11 | Dark
12 | Set by Battery Saver
13 | System default
14 | Selection menu options
15 | Use pop-up mode
16 | Show DeepL button in the text selection menu
17 | App options
18 | Automatically delete cookies when exiting the app
19 | About this app
20 | Version
21 |
22 |
--------------------------------------------------------------------------------
/README_ZH.md:
--------------------------------------------------------------------------------
1 | #### [EN](https://github.com/sakusaku3939/DeepLAndroid#readme) | [JA](README_JA.md) | [RU](README_RU.md) | [DE](README_DE.md) | [FR](README_FR.md) | [IT](README_IT.md) | [TR](README_TR.md) | [ZH](README_ZH.md) | [CS](README_CS.md)
2 | # DeepL Android
3 |     
4 |
5 | 这是一个非官方的 DeepL 翻译 Android 应用。
6 | 它使用了 WebView 技术,这是 Android 图形用户界面的一个组件,使其看起来像一个原生应用。
7 | 为了隐藏所有不必要的元素,只保留最必要的部分,使用了 JavaScript。
8 |
9 | [
](https://f-droid.org/packages/com.example.deeplviewer)
12 |
13 | 
14 |
15 | ## 功能
16 | - 通过 WebView 仅显示翻译屏幕。
17 | - 可切换至深色模式。
18 | - 从选择文本菜单启动 DeepL。
19 | - 长按应用图标以访问设置屏幕。
20 |
21 | ## 注意事项
22 | 此项目已使用 DeepL 翻译。如果有任何翻译错误,请随时发送拉取请求!
23 |
24 | ## 许可证
25 | [MIT](https://github.com/sakusaku3939/DeepLAndroid/blob/master/LICENSE)
26 |
--------------------------------------------------------------------------------
/app/src/main/res/values-cs/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | DeepL
4 | Obrázek
5 | Text byl zkopírován
6 | Ups, nepodařilo se připojit k serveru DeepL
7 | Nastavení
8 | Nastavení motivu
9 | Motiv
10 | Světlý
11 | Tmavý
12 | Nastavení podle spořiče baterie
13 | Výchozí nastavení systému
14 | Možnosti nabídky výběru
15 | Použití režimu vyskakovacího okna
16 | Zobrazit tlačítko DeepL v nabídce výběru textu
17 | Možnosti aplikace
18 | Automatické mazání souborů cookie při ukončení aplikace
19 | O této aplikaci
20 | Verze
21 |
22 |
--------------------------------------------------------------------------------
/README_JA.md:
--------------------------------------------------------------------------------
1 | #### [EN](https://github.com/sakusaku3939/DeepLAndroid#readme) | [JA](README_JA.md) | [RU](README_RU.md) | [DE](README_DE.md) | [FR](README_FR.md) | [IT](README_IT.md) | [TR](README_TR.md) | [ZH](README_ZH.md) | [CS](README_CS.md)
2 | # DeepL Android
3 |     
4 |
5 | DeepL翻訳の非公式Androidアプリです。
6 | AndroidのGUIコンポーネントの「WebView」といわれる技術を使用してDeepL翻訳をネイティブアプリ風にしたものです。
7 | 必要な要素以外はJavaScriptを使用して隠しています。
8 |
9 | [
](https://f-droid.org/packages/com.example.deeplviewer)
12 |
13 | 
14 |
15 | ## 機能一覧
16 | - 翻訳画面のみをWebViewで表示
17 | - ダークモードへの切り替え
18 | - 選択メニューからDeepLアプリを起動
19 | - アプリアイコンを長押しで設定画面に入る
20 |
21 | ## Notes
22 | このプロジェクトはDeepLを利用して翻訳されています。もし誤訳等がありましたら、お気軽にプルリクエストを送ってください!
23 |
24 | ## License
25 | [MIT](https://github.com/sakusaku3939/DeepLAndroid/blob/master/LICENSE)
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/deeplviewer/config/WebViewConfig.kt:
--------------------------------------------------------------------------------
1 | package com.example.deeplviewer.config
2 |
3 | import android.annotation.SuppressLint
4 | import android.webkit.WebSettings
5 | import android.webkit.WebView
6 |
7 | object WebViewConfig {
8 | @SuppressLint("SetJavaScriptEnabled")
9 | fun applyBasicSettings(webView: WebView) {
10 | webView.settings.apply {
11 | javaScriptEnabled = true
12 | domStorageEnabled = true
13 |
14 | // cache settings
15 | cacheMode = WebSettings.LOAD_DEFAULT
16 | }
17 | }
18 |
19 | @SuppressLint("SetJavaScriptEnabled")
20 | fun applyOptimizedSettings(webView: WebView) {
21 | webView.settings.apply {
22 | // security settings
23 | allowFileAccess = false
24 | allowContentAccess = false
25 | setGeolocationEnabled(false)
26 |
27 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
28 | // off screen pre-rasterization (API 23+)
29 | offscreenPreRaster = true
30 | }
31 |
32 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
33 | // safe browsing (API 26+)
34 | safeBrowsingEnabled = true
35 | }
36 |
37 | // mixed content mode
38 | mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/network_err.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
27 |
28 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/root_preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/deeplviewer/App.kt:
--------------------------------------------------------------------------------
1 | package com.example.deeplviewer
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.content.pm.ShortcutManager
6 | import android.os.Build
7 | import androidx.annotation.RequiresApi
8 | import androidx.appcompat.app.AppCompatDelegate
9 |
10 | class App : Application() {
11 |
12 | override fun onCreate() {
13 | super.onCreate()
14 | switchTheme()
15 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
16 | removeOldAppShortcut()
17 | }
18 | }
19 |
20 | private fun switchTheme() {
21 | val config = getSharedPreferences("config", Context.MODE_PRIVATE)
22 | var darkThemeMode =
23 | config.getString(
24 | getString(R.string.key_dark_mode),
25 | AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM.toString()
26 | )!!
27 |
28 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && darkThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM.toString()) {
29 | darkThemeMode = AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY.toString()
30 | }
31 | AppCompatDelegate.setDefaultNightMode(darkThemeMode.toInt())
32 | }
33 |
34 | @RequiresApi(Build.VERSION_CODES.N_MR1)
35 | private fun removeOldAppShortcut() {
36 | val shortcutManager = getSystemService(ShortcutManager::class.java)
37 | shortcutManager.removeDynamicShortcuts(
38 | listOf(
39 | "theme"//version 5.0(17) Used to launch this app with darkTheme or lightTheme.
40 | )
41 | )
42 | }
43 | }
--------------------------------------------------------------------------------
/README_RU.md:
--------------------------------------------------------------------------------
1 | #### [EN](https://github.com/sakusaku3939/DeepLAndroid#readme) | [JA](README_JA.md) | [RU](README_RU.md) | [DE](README_DE.md) | [FR](README_FR.md) | [IT](README_IT.md) | [TR](README_TR.md) | [ZH](README_ZH.md) | [CS](README_CS.md)
2 | # DeepL Android
3 |     
4 |
5 | Это неофициальное Android-приложение для DeepL переводчика.
6 | Оно использует технологию WebView, компонент интерфейса Android, чтобы выглядеть как нативное приложение.
7 | Чтобы скрыть все ненужные элементы, используется JavaScript.
8 |
9 | [
](https://f-droid.org/packages/com.example.deeplviewer)
12 |
13 | 
14 |
15 | ## Функции
16 | - Показывать только экран перевода через WebView.
17 | - Переключение в темный режим.
18 | - Запустить DeepL из меню выделенного текста.
19 | - Откройте экран настроек, нажав и удерживая значок приложения.
20 |
21 | ## Примечание
22 | Этот проект был переведен при помощи DeepL. Если вы нашли ошибку, можете отправить pull request.
23 |
24 | ## Лицензия
25 | [MIT](https://github.com/sakusaku3939/DeepLAndroid/blob/master/LICENSE)
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #### [EN](https://github.com/sakusaku3939/DeepLAndroid#readme) | [JA](README_JA.md) | [RU](README_RU.md) | [DE](README_DE.md) | [FR](README_FR.md) | [IT](README_IT.md) | [TR](README_TR.md) | [ZH](README_ZH.md) | [CS](README_CS.md)
2 | # DeepL Android
3 |     
4 |
5 | This is an unofficial Android app for DeepL translation.
6 | It uses a technology called WebView, an Android GUI component, to make it look like a native app.
7 | JavaScript is used to hide all but the most necessary elements.
8 |
9 | [
](https://f-droid.org/packages/com.example.deeplviewer)
12 |
13 | 
14 |
15 | ## Features
16 | - Display only the translation screen by WebView.
17 | - Switching to dark mode.
18 | - Launch DeepL from floating text selection action.
19 | - Long press on the application icon to access the settings screen.
20 |
21 | ## Notes
22 | This project has been translated using DeepL. If there are any mistranslations, please feel free to send a pull request!
23 |
24 | ## License
25 | [MIT](https://github.com/sakusaku3939/DeepLAndroid/blob/master/LICENSE)
26 |
--------------------------------------------------------------------------------
/README_TR.md:
--------------------------------------------------------------------------------
1 | #### [EN](https://github.com/sakusaku3939/DeepLAndroid#readme) | [JA](README_JA.md) | [RU](README_RU.md) | [DE](README_DE.md) | [FR](README_FR.md) | [IT](README_IT.md) | [TR](README_TR.md) | [ZH](README_ZH.md) | [CS](README_CS.md)
2 | # DeepL Android
3 |     
4 |
5 | DeepL çeviri için resmi olmayan bir Android uygulamasıdır.
6 | Yerel bir uygulama gibi görünmesini sağlamak için bir Android Arayüz bileşeni olan WebView adlı bir teknoloji kullanır.
7 | JavaScript gereksiz öğeleri gizlemek için kullanılır.
8 |
9 | [
](https://f-droid.org/packages/com.example.deeplviewer)
12 |
13 | 
14 |
15 | ## Özellikler
16 | - WebView ile yalnızca çeviri ekranını görüntüleyin.
17 | - Karanlık moda geçme.
18 | - Kayan metin seçimi eyleminden DeepL'i başlatın.
19 | - Uygulama simgesine basılı tutarak ayarlar ekranına erişin.
20 |
21 | ## Notlar
22 | Bu proje DeepL kullanılarak çevrilmiştir. Herhangi bir yanlış çeviri varsa, lütfen bir çekme isteği göndermekten çekinmeyin!
23 |
24 | ## Lisans
25 | [MIT](https://github.com/sakusaku3939/DeepLAndroid/blob/master/LICENSE)
26 |
--------------------------------------------------------------------------------
/README_CS.md:
--------------------------------------------------------------------------------
1 | #### [EN](https://github.com/sakusaku3939/DeepLAndroid#readme) | [JA](README_JA.md) | [RU](README_RU.md) | [DE](README_DE.md) | [FR](README_FR.md) | [IT](README_IT.md) | [TR](README_TR.md) | [ZH](README_ZH.md) | [CS](README_CS.md)
2 | # DeepL Android
3 |     
4 |
5 | Toto je neoficiální Android aplikace pro DeepL překlady.
6 | Využívá technologii WebView, komponentu grafického uživatelského rozhraní systému Android, aby vypadala jako nativní aplikace.
7 | JavaScript se používá ke skrytí všech prvků kromě těch nejnutnějších
8 |
9 | [
](https://f-droid.org/packages/com.example.deeplviewer)
12 |
13 | 
14 |
15 | ## Funkce
16 | - Zobrazení pouze obrazovky překladu pomocí WebView.
17 | - Přepínání do tmavého režimu.
18 | - Spuštění DeepL z akce výběru v plovoucím textu.
19 | - Dlouhý stisk ikony aplikace umožňuje otevřít obrazovku s nastavením.
20 |
21 | ## Poznámky
22 | Tento projekt byl přeložen pomocí DeepL. Pokud se v něm vyskytnou nějaké chyby v překladu, neváhejte poslat pull request!
23 |
24 | ## Licence
25 | [MIT](https://github.com/sakusaku3939/DeepLAndroid/blob/master/LICENSE)
26 |
--------------------------------------------------------------------------------
/README_IT.md:
--------------------------------------------------------------------------------
1 | #### [EN](https://github.com/sakusaku3939/DeepLAndroid#readme) | [JA](README_JA.md) | [RU](README_RU.md) | [DE](README_DE.md) | [FR](README_FR.md) | [IT](README_IT.md) | [TR](README_TR.md) | [ZH](README_ZH.md) | [CS](README_CS.md)
2 | # DeepL Android
3 |     
4 |
5 | Questa è un'applicazione Android non ufficiale per traduzioni DeepL.
6 | Usa una tecnologia chiamata WebView, una componente GUI di Android, per farla sembrare come un'app nativa.
7 | JavaScript è usato per nascondere tutto tranne gli elementi più necessari.
8 |
9 | [
](https://f-droid.org/packages/com.example.deeplviewer)
12 |
13 | 
14 |
15 | ## Funzionalità
16 | - Mostra solo la schermata di traduzione tramite WebView.
17 | - Cambiare in modalità scura.
18 | - Lancia DeepL dall'azione di selezione del testo.
19 | - Accedere alla schermata delle impostazioni tenendo premuta l'icona dell'applicazione.
20 |
21 | ## Note
22 | Questo progetto è stato tradotto usando DeepL. Se ci sono errori di traduzione, sentitevi liberi di inviare una pull request!
23 |
24 | ## Licenza
25 | [MIT](https://github.com/sakusaku3939/DeepLAndroid/blob/master/LICENSE)
26 |
--------------------------------------------------------------------------------
/README_DE.md:
--------------------------------------------------------------------------------
1 | #### [EN](https://github.com/sakusaku3939/DeepLAndroid#readme) | [JA](README_JA.md) | [RU](README_RU.md) | [DE](README_DE.md) | [FR](README_FR.md) | [IT](README_IT.md) | [TR](README_TR.md) | [ZH](README_ZH.md) | [CS](README_CS.md)
2 | # DeepL Android
3 |     
4 |
5 | Dies ist eine inoffizielle Android-App für den DeepL-Übersetzer.
6 | Es verwendet eine Technologie namens WebView, eine Android-GUI-Komponente, um es wie eine native App aussehen zu lassen.
7 | JavaScript wird verwendet, um alle bis auf die notwendigsten Elemente auszublenden.
8 |
9 | [
](https://f-droid.org/packages/com.example.deeplviewer)
12 |
13 | 
14 |
15 | ## Funktionen
16 |
17 | - Den Übersetzungsbildschirm im WebView anzeigen lassen.
18 | - In den Dunkelmodus wechseln.
19 | - Starten Sie DeepL über die schwebende Textauswahlaktion.
20 | - Rufen Sie den Einstellungsbildschirm auf, indem Sie das Anwendungssymbol drücken und gedrückt halten.
21 |
22 | ## Anmerkung
23 | Dieses Projekt wurde mit DeepL übersetzt. Sollte es irgendwelche Übersetzungsfehler geben, erstellen Sie gerne einen Pull Request!
24 |
25 | ## Lizenz
26 | [MIT](https://github.com/sakusaku3939/DeepLAndroid/blob/master/LICENSE)
27 |
--------------------------------------------------------------------------------
/README_FR.md:
--------------------------------------------------------------------------------
1 | #### [EN](https://github.com/sakusaku3939/DeepLAndroid#readme) | [JA](README_JA.md) | [RU](README_RU.md) | [DE](README_DE.md) | [FR](README_FR.md) | [IT](README_IT.md) | [TR](README_TR.md) | [ZH](README_ZH.md) | [CS](README_CS.md)
2 | # DeepL Android
3 |     
4 |
5 | Il s'agit d'une application Android non officielle pour le traducteur DeepL.
6 | Elle utilise une technologie appelée WebView, un composant d'interface graphique d'Android, afin de la faire ressembler à une application native.
7 | JavaScript est utilisé pour masquer tous les éléments à l'exception des plus nécessaires.
8 |
9 |
10 | [
](https://f-droid.org/packages/com.example.deeplviewer)
13 |
14 | 
15 |
16 | ## Fonctionnalités:
17 |
18 | - Afficher uniquement l'écran de traduction via WebView
19 | - Passage en mode sombre
20 | - Lancer DeepL par une action du menu flottant de sélection de texte
21 | - Accédez à l'écran des paramètres en appuyant sur l'icône de l'application et en la maintenant enfoncée
22 |
23 | ## Notes
24 | Ce projet a été traduit à l'aide de DeepL.
25 | S'il y a des erreurs de traduction, n'hésitez pas à envoyer une demande de modification!
26 |
27 | ## License
28 | [MIT](https://github.com/sakusaku3939/DeepLAndroid/blob/master/LICENSE)
29 |
--------------------------------------------------------------------------------
/app/src/main/assets/DeepL_Logo_lightBlue_v2.svg:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/app/src/main/assets/hide-elements.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | const HIDE_SELECTORS = `
3 | header,
4 | nav[class^="md"],
5 | h2#translation-modes-heading + div > div:first-child,
6 | div[data-testid="translator"] > div:not(:first-child),
7 | footer,
8 | div[data-testid="chrome-extension-toast"],
9 | div[data-testid="firefox-extension-toast"],
10 | div[data-testid="write-banner"],
11 | div[id="cookieBanner"],
12 | div[aria-labelledby="app-stores-banner-description"],
13 | aside
14 | `;
15 |
16 | function injectCSS(rules) {
17 | const style = document.createElement('style');
18 | style.innerHTML = rules;
19 | document.head.appendChild(style);
20 | }
21 |
22 | /* wait for the Head element to be available */
23 | function waitForHead(callback) {
24 | if (document.head) {
25 | callback();
26 | return;
27 | }
28 |
29 | const observer = new MutationObserver(function (mutations) {
30 | if (document.head) {
31 | observer.disconnect();
32 | callback();
33 | }
34 | });
35 |
36 | observer.observe(document, {
37 | childList: true,
38 | subtree: true
39 | });
40 | }
41 |
42 | /* initially hide elements with `visibility: hidden` */
43 | window.hideElementsInitially = function () {
44 | waitForHead(function () {
45 | injectCSS(HIDE_SELECTORS + ` { visibility: hidden !important; }`);
46 | });
47 | };
48 |
49 | /* finally hide elements with `display: none` */
50 | window.hideElementsFinal = function () {
51 | injectCSS(`
52 | * { -webkit-tap-highlight-color: rgba(0, 0, 0,.1); }
53 | ${HIDE_SELECTORS} {
54 | display: none !important;
55 | height: 0;
56 | width: 0;
57 | overflow: hidden;
58 | }
59 | `);
60 | };
61 | })();
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/example/deeplviewer/WebViewScreenshotTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.deeplviewer
2 |
3 | import android.os.SystemClock
4 | import android.webkit.WebView
5 | import androidx.test.ext.junit.rules.ActivityScenarioRule
6 | import androidx.test.ext.junit.runners.AndroidJUnit4
7 | import com.example.deeplviewer.activity.MainActivity
8 | import com.example.deeplviewer.webview.MyWebViewClient
9 | import com.karumi.shot.ScreenshotTest
10 | import org.junit.Rule
11 | import org.junit.Test
12 | import org.junit.runner.RunWith
13 | import java.util.concurrent.CountDownLatch
14 | import java.util.concurrent.TimeUnit
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | class WebViewScreenshotTest : ScreenshotTest {
18 |
19 | @get:Rule
20 | val activityRule = ActivityScenarioRule(MainActivity::class.java)
21 |
22 | @Test
23 | fun captureWebViewTopScreen() {
24 | waitForWebViewLoad()
25 |
26 | activityRule.scenario.onActivity { activity ->
27 | val webView = activity.findViewById(R.id.webview)
28 | compareScreenshot(webView, name = "webview_top_screen")
29 | }
30 | }
31 |
32 | private fun waitForWebViewLoad() {
33 | val latch = CountDownLatch(1)
34 | var webViewClient: MyWebViewClient? = null
35 |
36 | activityRule.scenario.onActivity { activity ->
37 | webViewClient = activity.testWebViewClient
38 | webViewClient?.loadFinishedListener = {
39 | latch.countDown()
40 | }
41 | }
42 |
43 | checkNotNull(webViewClient) { "WebViewClient is not available" }
44 |
45 | // Wait for load completion (max 30 seconds)
46 | val loadCompleted = latch.await(30, TimeUnit.SECONDS)
47 |
48 | if (!loadCompleted) {
49 | throw AssertionError("WebView load timeout: failed to complete within 30 seconds")
50 | }
51 |
52 | // Wait for rendering to complete (using SystemClock for instrumentation tests)
53 | SystemClock.sleep(8000)
54 | }
55 | }
--------------------------------------------------------------------------------
/app/src/main/res/raw/isrg_root_x1.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
3 | TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
4 | cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
5 | WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
6 | ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
7 | MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
8 | h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
9 | 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
10 | A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
11 | T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
12 | B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
13 | B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
14 | KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
15 | OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
16 | jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
17 | qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
18 | rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
19 | HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
20 | hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
21 | ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
22 | 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
23 | NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
24 | ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
25 | TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
26 | jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
27 | oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
28 | 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
29 | mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
30 | emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
31 | -----END CERTIFICATE-----
32 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
21 |
22 |
25 |
26 |
29 |
30 |
33 |
34 |
37 |
38 |
41 |
42 |
46 |
47 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.10.1"
3 | kotlin = "2.1.21"
4 |
5 | androidx-core-ktx = "1.16.0"
6 | androidx-appcompat = "1.7.0"
7 | androidx-webkit = "1.13.0"
8 | androidx-preference = "1.2.1"
9 | androidx-lifecycle = "2.9.0"
10 | error-prone = "2.30.0"
11 | material = "1.12.0"
12 | shimmer = "0.5.0"
13 | junit = "4.13.2"
14 | androidx-test-ext-junit = "1.2.1"
15 | androidx-test-espresso = "3.6.1"
16 | androidx-test-runner = "1.6.2"
17 | androidx-test-rules = "1.6.1"
18 | shot="6.1.0"
19 |
20 | [libraries]
21 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
22 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" }
23 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
24 | androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "androidx-webkit" }
25 | androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "androidx-preference" }
26 | androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" }
27 | error-prone-annotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "error-prone" }
28 | material = { module = "com.google.android.material:material", version.ref = "material" }
29 | shimmer = { module = "com.facebook.shimmer:shimmer", version.ref = "shimmer" }
30 | junit = { module = "junit:junit", version.ref = "junit" }
31 | androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" }
32 | androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" }
33 | androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" }
34 | androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-rules" }
35 | shot-android = { module = "com.karumi:shot-android", version.ref = "shot" }
36 | shot-classpath = { module = "com.karumi:shot", version.ref = "shot" }
37 |
38 | [plugins]
39 | android-application = { id = "com.android.application", version.ref = "agp" }
40 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
41 | shot = { id="shot", version.ref="shot" }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/deeplviewer/helper/CookieManagerHelper.kt:
--------------------------------------------------------------------------------
1 | package com.example.deeplviewer.helper
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import android.webkit.CookieManager
6 | import android.webkit.WebView
7 | import com.example.deeplviewer.R
8 | import java.net.URLEncoder
9 | import androidx.core.content.edit
10 |
11 | class CookieManagerHelper {
12 | companion object {
13 | private fun getCookieManager(): CookieManager {
14 | return CookieManager.getInstance()
15 | }
16 | }
17 |
18 | fun addPrivacyCookie() {
19 | val privacyValue = "{\"v\":2,\"t\":${System.currentTimeMillis().div(1000)},\"m\":\"STRICT\",\"consent\":[\"NECESSARY\"]}"
20 |
21 | getCookieManager().setCookie(
22 | ".deepl.com",
23 | "privacySettings=${URLEncoder.encode(privacyValue, Charsets.UTF_8.name())}"
24 | )
25 | }
26 |
27 | fun saveCookies(context: Context, webView: WebView) {
28 | val cookieManager = getCookieManager()
29 | val isCookieAutoDeleted = context.getSharedPreferences("config", Context.MODE_PRIVATE)
30 | .getBoolean(context.getString(R.string.key_auto_delete_cookies), false)
31 |
32 | if (isCookieAutoDeleted) {
33 | clearCookies()
34 | } else {
35 | cookieManager.acceptCookie()
36 | cookieManager.setAcceptThirdPartyCookies(webView, true)
37 | cookieManager.flush()
38 | }
39 | }
40 |
41 |
42 | // Disable cookie values in SharedPreferences according to the bug fix for cookie expiration in v8.5
43 | fun migrateCookie(context: Context) {
44 | val sharedPreferences = context.getSharedPreferences("DeepLCookies", Context.MODE_PRIVATE)
45 | val savedCookie = sharedPreferences.getString("cookie", null)
46 |
47 | if (savedCookie != null) {
48 | clearCookies()
49 | sharedPreferences.edit { clear() }
50 | }
51 | }
52 |
53 | private fun clearCookies() {
54 | getCookieManager().removeAllCookies { success ->
55 | if (success) {
56 | Log.d("CookieManagerHelper", "Cookies successfully removed.")
57 | } else {
58 | Log.e("CookieManagerHelper", "Failed to remove cookies.")
59 | }
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
20 |
21 |
25 |
26 |
35 |
36 |
40 |
41 |
51 |
52 |
53 |
54 |
55 |
56 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.shot)
5 | }
6 |
7 | android {
8 | namespace = "com.example.deeplviewer"
9 | compileSdk = 36
10 |
11 | defaultConfig {
12 | applicationId = "com.example.deeplviewer"
13 | minSdk = 21
14 | targetSdk = 36
15 | versionCode = 46
16 | versionName = "9.2.1"
17 |
18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
19 | testInstrumentationRunner = "com.karumi.shot.ShotTestRunner"
20 |
21 | vectorDrawables {
22 | useSupportLibrary = true
23 | }
24 | }
25 |
26 | buildTypes {
27 | debug {
28 | isMinifyEnabled = false
29 | isShrinkResources = false
30 | proguardFiles(
31 | getDefaultProguardFile("proguard-android-optimize.txt"),
32 | "proguard-rules.pro"
33 | )
34 | applicationIdSuffix = ".debug"
35 | }
36 | release {
37 | isMinifyEnabled = true
38 | isShrinkResources = true
39 | proguardFiles(
40 | getDefaultProguardFile("proguard-android-optimize.txt"),
41 | "proguard-rules.pro"
42 | )
43 | }
44 | }
45 |
46 | buildFeatures {
47 | buildConfig = true
48 | viewBinding = true
49 | }
50 |
51 | compileOptions {
52 | sourceCompatibility = JavaVersion.VERSION_1_8
53 | targetCompatibility = JavaVersion.VERSION_1_8
54 | }
55 |
56 | kotlinOptions {
57 | jvmTarget = "1.8"
58 | }
59 |
60 | lint {
61 | disable.add("MissingTranslation")
62 | }
63 | }
64 |
65 | shot {
66 | applicationId = "com.example.deeplviewer"
67 | tolerance = 0.2
68 | }
69 |
70 | dependencies {
71 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
72 | implementation(libs.kotlin.stdlib)
73 | implementation(libs.androidx.core.ktx)
74 | implementation(libs.androidx.appcompat)
75 | implementation(libs.androidx.webkit)
76 | implementation(libs.androidx.preference.ktx)
77 | implementation(libs.androidx.lifecycle.viewmodel.ktx)
78 | implementation(libs.error.prone.annotations)
79 | implementation(libs.material)
80 | implementation(libs.shimmer)
81 |
82 | testImplementation(libs.junit)
83 | androidTestImplementation(libs.androidx.test.ext.junit)
84 | androidTestImplementation(libs.androidx.test.espresso.core)
85 | androidTestImplementation(libs.androidx.test.runner)
86 | androidTestImplementation(libs.androidx.test.rules)
87 | androidTestImplementation(libs.shot.android)
88 | testImplementation(kotlin("test"))
89 | }
--------------------------------------------------------------------------------
/app/src/main/assets/DeepL_Text_light.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/.github/workflows/capture_webview_screenshots.yml:
--------------------------------------------------------------------------------
1 | name: Capture WebView Screenshots
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | permissions:
7 | contents: write
8 |
9 | jobs:
10 | generate-screenshots:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v4
16 | with:
17 | persist-credentials: true
18 |
19 | - name: Set up JDK 17
20 | uses: actions/setup-java@v4
21 | with:
22 | java-version: '17'
23 | distribution: 'temurin'
24 |
25 | - name: Setup Android SDK
26 | uses: android-actions/setup-android@v3
27 |
28 | - name: Accept Android SDK licenses
29 | run: yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses
30 |
31 | - name: Cache Gradle dependencies
32 | uses: actions/cache@v3
33 | with:
34 | path: |
35 | ~/.gradle/caches
36 | ~/.gradle/wrapper
37 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
38 | restore-keys: |
39 | ${{ runner.os }}-gradle-
40 |
41 | - name: Install Android SDK components
42 | run: |
43 | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0"
44 |
45 | - name: Enable KVM group perms
46 | run: |
47 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
48 | sudo udevadm control --reload-rules
49 | sudo udevadm trigger --name-match=kvm
50 |
51 | - name: AVD cache
52 | uses: actions/cache@v3
53 | id: avd-cache
54 | with:
55 | path: |
56 | ~/.android/avd/*
57 | ~/.android/adb*
58 | key: avd-35-x86_64
59 |
60 | - name: Create AVD and generate snapshot for caching
61 | if: steps.avd-cache.outputs.cache-hit != 'true'
62 | uses: reactivecircus/android-emulator-runner@v2
63 | with:
64 | api-level: 35
65 | arch: x86_64
66 | target: google_apis
67 | force-avd-creation: false
68 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
69 | disable-animations: false
70 | script: echo "Generated AVD snapshot for caching."
71 |
72 | - name: Generate and record screenshots
73 | uses: reactivecircus/android-emulator-runner@v2
74 | with:
75 | api-level: 35
76 | arch: x86_64
77 | target: google_apis
78 | force-avd-creation: false
79 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
80 | disable-animations: false
81 | script: |
82 | ./gradlew executeScreenshotTests -Precord
83 |
84 | - name: Commit and push new screenshots
85 | run: |
86 | git config --local user.email "action@github.com"
87 | git config --local user.name "GitHub Action"
88 | git add app/screenshots/
89 | git add **/screenshots/
90 | if git diff --staged --quiet; then
91 | echo "No screenshot changes to commit"
92 | else
93 | git commit -m "Update screenshots"
94 | git push
95 | fi
96 |
97 | - name: Upload screenshots
98 | uses: actions/upload-artifact@v4
99 | if: always()
100 | with:
101 | name: screenshots-${{ github.run_id }}
102 | path: |
103 | app/screenshots/
104 | **/screenshots/
105 | retention-days: 30
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
16 |
17 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
52 |
53 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
85 |
86 |
93 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/deeplviewer/activity/SettingsActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.deeplviewer.activity
2 |
3 | import android.content.ComponentName
4 | import android.content.Intent
5 | import android.content.pm.PackageManager
6 | import android.os.Build
7 | import android.os.Bundle
8 | import android.view.MenuItem
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.appcompat.app.AppCompatDelegate
11 | import androidx.appcompat.widget.Toolbar
12 | import androidx.core.net.toUri
13 | import androidx.preference.DropDownPreference
14 | import androidx.preference.Preference
15 | import androidx.preference.PreferenceFragmentCompat
16 | import androidx.preference.SwitchPreference
17 | import com.example.deeplviewer.BuildConfig
18 | import com.example.deeplviewer.R
19 |
20 |
21 | class SettingsActivity : AppCompatActivity() {
22 |
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | setContentView(R.layout.settings_activity)
26 | val toolbar: Toolbar = findViewById(R.id.settingsToolbar)
27 | setSupportActionBar(toolbar)
28 | supportActionBar?.setDisplayHomeAsUpEnabled(true)
29 | }
30 |
31 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
32 | if (item.itemId == android.R.id.home) {
33 | finish()
34 | }
35 | return super.onOptionsItemSelected(item)
36 | }
37 |
38 | class SettingsFragment : PreferenceFragmentCompat() {
39 |
40 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
41 | preferenceManager.sharedPreferencesName = "config"
42 | setPreferencesFromResource(R.xml.root_preferences, rootKey)
43 |
44 | val darkMode = findPreference(getString(R.string.key_dark_mode))
45 | darkMode?.setOnPreferenceChangeListener { _, newValue ->
46 |
47 | var darkThemeMode = newValue as String
48 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && darkThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM.toString()) {
49 | darkThemeMode = AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY.toString()
50 | }
51 | AppCompatDelegate.setDefaultNightMode(darkThemeMode.toInt())
52 | return@setOnPreferenceChangeListener true
53 | }
54 |
55 | val translateButton =
56 | findPreference(getString(R.string.key_switch_translate_button))
57 | translateButton?.setOnPreferenceChangeListener { _, newValue ->
58 | val packageName = requireContext().packageName
59 | val packageManager = requireContext().packageManager
60 |
61 | val showComponentName =
62 | ComponentName(packageName, "com.example.deeplviewer.FloatingTextSelection_show")
63 | val hideComponentName =
64 | ComponentName(packageName, "com.example.deeplviewer.FloatingTextSelection_hide")
65 |
66 | if (newValue == true) {
67 | packageManager.setComponentEnabledSetting(
68 | showComponentName,
69 | PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
70 | PackageManager.DONT_KILL_APP
71 | )
72 | packageManager.setComponentEnabledSetting(
73 | hideComponentName,
74 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
75 | PackageManager.DONT_KILL_APP
76 | )
77 | } else {
78 | packageManager.setComponentEnabledSetting(
79 | hideComponentName,
80 | PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
81 | PackageManager.DONT_KILL_APP
82 | )
83 | packageManager.setComponentEnabledSetting(
84 | showComponentName,
85 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
86 | PackageManager.DONT_KILL_APP
87 | )
88 | }
89 | return@setOnPreferenceChangeListener true
90 | }
91 |
92 | val versionButton = findPreference(getString(R.string.key_version))
93 | versionButton?.summary = "v${BuildConfig.VERSION_NAME}"
94 | versionButton?.setOnPreferenceClickListener {
95 | startActivity(
96 | Intent(Intent.ACTION_VIEW, getString(R.string.link_github_release).toUri())
97 | )
98 | return@setOnPreferenceClickListener true
99 | }
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/.github/workflows/screenshot_diff_check.yml:
--------------------------------------------------------------------------------
1 | name: Screenshot Diff Check
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: "0 0 * * *"
7 | workflow_run:
8 | workflows: [ "Capture WebView Screenshots" ]
9 | types:
10 | - completed
11 |
12 | permissions:
13 | contents: read
14 | pages: write
15 | id-token: write
16 |
17 | jobs:
18 | compare-screenshots:
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - name: Checkout code
23 | uses: actions/checkout@v4
24 |
25 | - name: Set up JDK 17
26 | uses: actions/setup-java@v4
27 | with:
28 | java-version: '17'
29 | distribution: 'temurin'
30 |
31 | - name: Setup Android SDK
32 | uses: android-actions/setup-android@v3
33 |
34 | - name: Accept Android SDK licenses
35 | run: yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses
36 |
37 | - name: Cache Gradle dependencies
38 | uses: actions/cache@v3
39 | with:
40 | path: |
41 | ~/.gradle/caches
42 | ~/.gradle/wrapper
43 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
44 | restore-keys: |
45 | ${{ runner.os }}-gradle-
46 |
47 | - name: Install Android SDK components
48 | run: |
49 | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0"
50 |
51 | - name: Enable KVM group perms
52 | run: |
53 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
54 | sudo udevadm control --reload-rules
55 | sudo udevadm trigger --name-match=kvm
56 |
57 | - name: AVD cache
58 | uses: actions/cache@v3
59 | id: avd-cache
60 | with:
61 | path: |
62 | ~/.android/avd/*
63 | ~/.android/adb*
64 | key: avd-35-x86_64
65 |
66 | - name: Create AVD and generate snapshot for caching
67 | if: steps.avd-cache.outputs.cache-hit != 'true'
68 | uses: reactivecircus/android-emulator-runner@v2
69 | with:
70 | api-level: 35
71 | arch: x86_64
72 | target: google_apis
73 | force-avd-creation: false
74 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
75 | disable-animations: false
76 | script: echo "Generated AVD snapshot for caching."
77 |
78 | - name: Run screenshot tests
79 | uses: reactivecircus/android-emulator-runner@v2
80 | with:
81 | api-level: 35
82 | arch: x86_64
83 | target: google_apis
84 | force-avd-creation: false
85 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
86 | disable-animations: false
87 | script: |
88 | ./gradlew executeScreenshotTests || true
89 |
90 | - name: Prepare report for GitHub Pages
91 | run: |
92 | mkdir -p public
93 | if [ -d "app/build/reports/shot/debug/verification" ]; then
94 | cp -r app/build/reports/shot/debug/verification/* public/
95 | else
96 | echo "No screenshot report found
" > public/index.html
97 | fi
98 | - name: Upload artifact for Pages
99 | uses: actions/upload-pages-artifact@v3
100 | with:
101 | path: ./public
102 |
103 | - name: Upload comparison results as artifact
104 | uses: actions/upload-artifact@v4
105 | if: always()
106 | with:
107 | name: screenshot-comparison-${{ github.run_id }}
108 | path: |
109 | app/build/reports/shot/debug/verification/
110 | public/
111 | retention-days: 30
112 |
113 | deploy:
114 | needs: compare-screenshots
115 | runs-on: ubuntu-latest
116 | environment:
117 | name: github-pages
118 | url: ${{ steps.deployment.outputs.page_url }}
119 |
120 | permissions:
121 | pages: write
122 | id-token: write
123 |
124 | steps:
125 | - name: Deploy to GitHub Pages
126 | id: deployment
127 | uses: actions/deploy-pages@v4
128 |
129 | check-result:
130 | needs: [ compare-screenshots, deploy ]
131 | runs-on: ubuntu-latest
132 | steps:
133 | - name: Download comparison artifact
134 | uses: actions/download-artifact@v4
135 | with:
136 | name: screenshot-comparison-${{ github.run_id }}
137 |
138 | - name: Fail job if diff found
139 | run: |
140 | REPORT=$(find . -type f -name "TEST-Shot.xml" | head -n 1)
141 | if [ -z "$REPORT" ]; then
142 | echo "❌ TEST-Shot.xml not found"
143 | exit 1
144 | fi
145 | if grep -q "
6 |
10 |
14 |
18 |
22 |
26 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/deeplviewer/activity/FloatingTextSelection.kt:
--------------------------------------------------------------------------------
1 | package com.example.deeplviewer.activity
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import android.view.View
8 | import android.view.animation.AlphaAnimation
9 | import android.webkit.WebView
10 | import androidx.appcompat.app.AppCompatActivity
11 | import com.example.deeplviewer.R
12 | import com.example.deeplviewer.config.WebViewConfig
13 | import com.example.deeplviewer.helper.UrlHelper
14 | import com.example.deeplviewer.webview.MyWebViewClient
15 | import com.example.deeplviewer.webview.NestedScrollWebView
16 | import com.example.deeplviewer.webview.WebAppInterface
17 | import com.facebook.shimmer.ShimmerFrameLayout
18 | import com.google.android.material.bottomsheet.BottomSheetDialog
19 |
20 |
21 | class FloatingTextSelection : AppCompatActivity() {
22 | private lateinit var webView: WebView
23 | private lateinit var webViewClient: MyWebViewClient
24 | private lateinit var layout: View
25 | private lateinit var startUrl: String
26 |
27 | override fun onCreate(savedInstanceState: Bundle?) {
28 | super.onCreate(savedInstanceState)
29 |
30 | startUrl = UrlHelper.buildStartUrl(this)
31 |
32 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
33 | val androidTranslateFloatingText =
34 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
35 | intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
36 | } else {
37 | null
38 | }
39 |
40 | val floatingText = (androidTranslateFloatingText
41 | ?: intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)) as String
42 | val config = getSharedPreferences("config", Context.MODE_PRIVATE)
43 | val usePopup = config.getBoolean(getString(R.string.key_switch_popup_mode), true)
44 |
45 | if (usePopup) {
46 | launchPopup(floatingText)
47 | } else {
48 | launchFullscreen(floatingText)
49 | }
50 | }
51 | }
52 |
53 | override fun onPause() {
54 | super.onPause()
55 | finish()
56 | }
57 |
58 | override fun finish() {
59 | super.finish()
60 | overridePendingTransition(0, 0)
61 | }
62 |
63 | /**
64 | * Launches the DeepL WebView in fullscreen mode with the selected text
65 | */
66 | private fun launchFullscreen(initialText: String) {
67 | val intent = Intent(this, MainActivity::class.java)
68 | intent.putExtra("FLOATING_TEXT", initialText)
69 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
70 | startActivity(intent)
71 | overridePendingTransition(0, 0)
72 | finish()
73 | }
74 |
75 | /**
76 | * Launches a popup with the DeepL WebView and the selected text
77 | */
78 | private fun launchPopup(initialText: String) {
79 | initializeWebView()
80 | setupWebViewClient()
81 |
82 | // Add JavaScript interface for communication
83 | webView.addJavascriptInterface(WebAppInterface(this), "Android")
84 |
85 | webViewClient.loadFinishedListener = {
86 | setWebViewHeight()
87 | showWebViewWithAnimation()
88 | }
89 |
90 | showBottomSheetDialog()
91 |
92 | // Load the initial URL
93 | val targetUrl = UrlHelper.buildWebViewUrl(startUrl, initialText)
94 | webView.loadUrl(targetUrl)
95 | }
96 |
97 | /**
98 | * Initializes the WebView
99 | */
100 | private fun initializeWebView() {
101 | layout = layoutInflater.inflate(R.layout.popup_layout, null)
102 | webView = layout.findViewById(R.id.webview)
103 |
104 | WebViewConfig.applyBasicSettings(webView)
105 | }
106 |
107 | /**
108 | * Sets up the WebViewClient
109 | */
110 | private fun setupWebViewClient() {
111 | webViewClient = MyWebViewClient(this)
112 | webView.webViewClient = webViewClient
113 | }
114 |
115 | /**
116 | * Shows the BottomSheetDialog with the WebView
117 | */
118 | @SuppressLint("RestrictedApi", "VisibleForTests")
119 | private fun showBottomSheetDialog() {
120 | val dialog = BottomSheetDialog(this)
121 | dialog.setContentView(layout)
122 | dialog.setOnDismissListener { finish() }
123 | dialog.behavior.disableShapeAnimations()
124 | dialog.show()
125 | }
126 |
127 | /**
128 | * Sets the height of the WebView to 70% of the screen height
129 | */
130 | private fun setWebViewHeight() {
131 | val displayMetrics = resources.displayMetrics
132 | val screenHeight = displayMetrics.heightPixels
133 | val webViewHeight = (screenHeight * 0.7).toInt()
134 |
135 | val layoutParams = webView.layoutParams
136 | layoutParams.height = webViewHeight
137 | webView.layoutParams = layoutParams
138 |
139 | webView.measure(
140 | View.MeasureSpec.makeMeasureSpec(webView.width, View.MeasureSpec.EXACTLY),
141 | View.MeasureSpec.makeMeasureSpec(webViewHeight, View.MeasureSpec.EXACTLY)
142 | )
143 | webView.layout(0, 0, webView.width, webViewHeight)
144 | }
145 |
146 | /**
147 | * Shows the WebView with a fade-in animation
148 | */
149 | private fun showWebViewWithAnimation() {
150 | val animation = AlphaAnimation(0.0F, 1.0F)
151 | animation.duration = 250
152 | webView.visibility = View.VISIBLE
153 | webView.startAnimation(animation)
154 | layout.findViewById(R.id.shimmer_view_container).hideShimmer()
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
22 |
26 |
30 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_monochrome.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
22 |
26 |
30 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/deeplviewer/activity/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.deeplviewer.activity
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Bundle
7 | import android.util.Log
8 | import android.webkit.WebView
9 | import android.widget.ImageButton
10 | import androidx.appcompat.app.AppCompatActivity
11 | import androidx.core.content.edit
12 | import com.example.deeplviewer.R
13 | import com.example.deeplviewer.config.WebViewConfig
14 | import com.example.deeplviewer.helper.CookieManagerHelper
15 | import com.example.deeplviewer.helper.UrlHelper
16 | import com.example.deeplviewer.webview.MyWebViewClient
17 | import com.example.deeplviewer.webview.WebAppInterface
18 |
19 | class MainActivity : AppCompatActivity() {
20 | private lateinit var webView: WebView
21 | private lateinit var webViewClient: MyWebViewClient
22 | private lateinit var startUrl: String
23 |
24 | val testWebViewClient: MyWebViewClient?
25 | get() = if (::webViewClient.isInitialized) webViewClient else null
26 |
27 | override fun onCreate(savedInstanceState: Bundle?) {
28 | super.onCreate(savedInstanceState)
29 | setContentView(R.layout.activity_main)
30 |
31 | startUrl = UrlHelper.buildStartUrl(this)
32 |
33 | initializeWebView()
34 | createWebView(intent, savedInstanceState)
35 | setupSettingsButton()
36 | }
37 |
38 | override fun onNewIntent(intent: Intent?) {
39 | super.onNewIntent(intent)
40 | createWebView(intent)
41 | }
42 |
43 | override fun onSaveInstanceState(outState: Bundle) {
44 | super.onSaveInstanceState(outState)
45 | saveWebViewState(outState)
46 | }
47 |
48 | override fun onDestroy() {
49 | if (::webView.isInitialized) {
50 | // Remove the WebView from its parent view and destroy it
51 | (webView.parent as? android.view.ViewGroup)?.removeView(webView)
52 | webView.destroy()
53 | }
54 | super.onDestroy()
55 | }
56 |
57 | /**
58 | * Creates and configures the WebView with the provided intent or saved instance state
59 | */
60 | private fun createWebView(intent: Intent?, savedInstanceState: Bundle? = null) {
61 | val receivedText = extractReceivedText(intent, savedInstanceState)
62 |
63 | setupWebViewClient()
64 | setupCookies()
65 |
66 | // Add JavaScript interface for communication
67 | webView.addJavascriptInterface(WebAppInterface(this), "Android")
68 |
69 | // Load the initial URL
70 | val targetUrl = UrlHelper.buildWebViewUrl(startUrl, receivedText)
71 | webView.loadUrl(targetUrl)
72 | }
73 |
74 | /**
75 | * Initializes the WebView
76 | */
77 | private fun initializeWebView() {
78 | webView = findViewById(R.id.webview)
79 | WebViewConfig.applyBasicSettings(webView)
80 | WebViewConfig.applyOptimizedSettings(webView)
81 | }
82 |
83 | /**
84 | * Extracts the text from the intent or saved instance state
85 | */
86 | private fun extractReceivedText(intent: Intent?, savedInstanceState: Bundle?): String {
87 | return savedInstanceState?.getString("SavedText")
88 | ?: intent?.getStringExtra("FLOATING_TEXT")
89 | ?: intent?.getStringExtra(Intent.EXTRA_TEXT)
90 | ?: ""
91 | }
92 |
93 | /**
94 | * Sets up the WebViewClient to handle page loading and animations
95 | */
96 | private fun setupWebViewClient() {
97 | webViewClient = MyWebViewClient(this)
98 | webView.webViewClient = webViewClient
99 | }
100 |
101 | /**
102 | * Sets up cookies for the WebView
103 | */
104 | private fun setupCookies() {
105 | try {
106 | CookieManagerHelper().apply {
107 | migrateCookie(this@MainActivity)
108 | addPrivacyCookie()
109 | }
110 | } catch (e: Exception) {
111 | Log.w("MainActivity", "Cookie setup failed", e)
112 | }
113 | }
114 |
115 | /**
116 | * Sets up the settings button to open the SettingsActivity
117 | */
118 | private fun setupSettingsButton() {
119 | findViewById(R.id.settingButton).setOnClickListener {
120 | val intent = Intent(this, SettingsActivity::class.java)
121 | startActivity(intent)
122 | }
123 | }
124 |
125 | /**
126 | * Saves the current state of the WebView to the outState bundle
127 | */
128 | private fun saveWebViewState(outState: Bundle) {
129 | val webView: WebView = findViewById(R.id.webview)
130 | val url = webView.url ?: ""
131 |
132 | // Extract the page type 'translator' or 'write' from the URL
133 | val pageTypeRegex = Regex("^https://www\\.deepl\\.com/.*/(translator|write).*$")
134 | val pageTypeMatch = pageTypeRegex.find(url)
135 | val pageType = pageTypeMatch?.groupValues?.get(1)
136 |
137 | pageType?.let {
138 | getSharedPreferences("config", Context.MODE_PRIVATE).edit {
139 | putString("pageType", it)
140 | }
141 |
142 | // Save the URL parameter if it changed
143 | val urlParam = url.substringAfter(it)
144 | val originUrlParam = startUrl.substringAfter(it)
145 | val isTextChanged = urlParam != originUrlParam
146 |
147 | if (isTextChanged && urlParam.isNotEmpty()) {
148 | val urlText = urlParam.substring(originUrlParam.length)
149 | val inputText = Uri.decode(urlText).replace("\\/", "/")
150 | outState.putString("SavedText", inputText)
151 | }
152 | }
153 |
154 | try {
155 | CookieManagerHelper().saveCookies(this, webView)
156 | } catch (e: Exception) {
157 | Log.w("MainActivity", "Cookie save failed", e)
158 | }
159 | }
160 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/deeplviewer/webview/MyWebViewClient.kt:
--------------------------------------------------------------------------------
1 | package com.example.deeplviewer.webview
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.res.Configuration
7 | import android.os.Build
8 | import android.util.Log
9 | import android.view.View
10 | import android.webkit.WebResourceError
11 | import android.webkit.WebResourceRequest
12 | import android.webkit.WebView
13 | import android.webkit.WebViewClient
14 | import android.widget.ImageButton
15 | import android.widget.Toast
16 | import androidx.annotation.RequiresApi
17 | import androidx.webkit.WebSettingsCompat
18 | import androidx.webkit.WebViewFeature
19 | import com.example.deeplviewer.R
20 | import com.example.deeplviewer.activity.MainActivity
21 | import androidx.core.content.edit
22 |
23 | class MyWebViewClient(
24 | private val activity: Activity,
25 | ) : WebViewClient() {
26 | private var isSplashFadeDone: Boolean = false
27 | private var param: String = "#en/en/"
28 |
29 | var loadFinishedListener: (() -> Unit)? = null
30 |
31 | override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
32 | val url = request?.url.toString()
33 | val regex = Regex("^https://www\\.deepl\\.com/.*/(translator|write).*$")
34 | val isDeepLTranslatorUrl = regex.matches(url)
35 |
36 | return !isDeepLTranslatorUrl
37 | }
38 |
39 | override fun onPageStarted(view: WebView, url: String, favicon: android.graphics.Bitmap?) {
40 | // initially hide elements with `visibility: hidden`
41 | view.loadJavaScript("hide-elements.js")
42 | view.evaluateJavascript("hideElementsInitially();", null)
43 | }
44 |
45 | override fun onPageFinished(view: WebView, url: String) {
46 | if (!isSplashFadeDone) {
47 | isSplashFadeDone = true
48 | loadFinishedListener?.invoke()
49 | }
50 |
51 | // finally hide elements with `display: none`
52 | view.evaluateJavascript("hideElementsFinal();", null)
53 |
54 | view.loadJavaScript("jquery-3.6.0.min.js")
55 |
56 | switchTheme(view)
57 | saveUrlParam(view.url)
58 | }
59 |
60 | override fun onReceivedError(
61 | view: WebView?,
62 | request: WebResourceRequest,
63 | error: WebResourceError?
64 | ) {
65 | if (request.isForMainFrame) {
66 | activity.setContentView(R.layout.network_err)
67 |
68 | val button: ImageButton = activity.findViewById(R.id.reload)
69 | val listener = ReloadButtonListener()
70 | button.setOnClickListener(listener)
71 |
72 | val errorDescription =
73 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) error?.description.toString() else ""
74 | Toast.makeText(activity, errorDescription, Toast.LENGTH_LONG).show()
75 | Log.e("onReceivedError", errorDescription)
76 | }
77 | }
78 |
79 | /**
80 | * Saves the URL parameter from the DeepL translator URL
81 | */
82 | private fun saveUrlParam(url: String?) {
83 | Regex("#(.+?)/(.+?)/").find(url ?: "")?.let {
84 | param = it.value
85 | activity.getSharedPreferences("config", Context.MODE_PRIVATE)
86 | .edit {
87 | putString("urlParam", param)
88 | }
89 | }
90 | }
91 |
92 | /**
93 | * loads a JavaScript file from the assets folder into the WebView
94 | */
95 | private fun WebView.loadJavaScript(fileName: String) {
96 | val jsCode = getAssetsText(this.context, fileName)
97 | this.evaluateJavascript(jsCode, null)
98 | }
99 |
100 | /**
101 | * Reads a text file from the assets folder
102 | */
103 | private fun getAssetsText(context: Context, fileName: String): String {
104 | return context.assets.open(fileName).reader(Charsets.UTF_8).use { it.readText() }
105 | }
106 |
107 | /**
108 | * Switches the WebView theme based on the system's dark mode setting
109 | */
110 | private fun switchTheme(view: WebView) {
111 | val uiMode = view.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
112 | if (uiMode != Configuration.UI_MODE_NIGHT_YES) return
113 |
114 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
115 | setAlgorithmicDark(view)
116 | } else {
117 | setForceDark(view)
118 | }
119 | }
120 |
121 | /**
122 | * Applies algorithmic darkening to the WebView if supported
123 | */
124 | @RequiresApi(Build.VERSION_CODES.Q)
125 | private fun setAlgorithmicDark(view: WebView) {
126 | if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
127 | WebSettingsCompat.setAlgorithmicDarkeningAllowed(view.settings, true)
128 | view.loadJavaScript("patch-darkThemeFix.js")
129 | } else {
130 | Toast.makeText(
131 | activity,
132 | "Dark mode cannot be used because ALGORITHMIC_DARKENING is not supported",
133 | Toast.LENGTH_LONG
134 | ).show()
135 | }
136 | }
137 |
138 | /**
139 | * Forces dark mode on the WebView
140 | */
141 | @Suppress("DEPRECATION")
142 | private fun setForceDark(view: WebView) {
143 | if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
144 | WebSettingsCompat.setForceDark(view.settings, WebSettingsCompat.FORCE_DARK_ON)
145 | view.loadJavaScript("patch-darkThemeFix.js")
146 | } else {
147 | Toast.makeText(
148 | activity,
149 | "Dark mode cannot be used because FORCE_DARK is not supported",
150 | Toast.LENGTH_LONG
151 | ).show()
152 | }
153 | }
154 |
155 | private inner class ReloadButtonListener : View.OnClickListener {
156 | override fun onClick(view: View) {
157 | val i = Intent(activity, MainActivity::class.java)
158 | activity.finish()
159 | activity.overridePendingTransition(0, 0)
160 | activity.startActivity(i)
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.3)
5 | addressable (2.8.0)
6 | public_suffix (>= 2.0.2, < 5.0)
7 | artifactory (3.0.15)
8 | atomos (0.1.3)
9 | aws-eventstream (1.4.0)
10 | aws-partitions (1.1196.0)
11 | aws-sdk-core (3.240.0)
12 | aws-eventstream (~> 1, >= 1.3.0)
13 | aws-partitions (~> 1, >= 1.992.0)
14 | aws-sigv4 (~> 1.9)
15 | base64
16 | bigdecimal
17 | jmespath (~> 1, >= 1.6.1)
18 | logger
19 | aws-sdk-kms (1.118.0)
20 | aws-sdk-core (~> 3, >= 3.239.1)
21 | aws-sigv4 (~> 1.5)
22 | aws-sdk-s3 (1.208.0)
23 | aws-sdk-core (~> 3, >= 3.234.0)
24 | aws-sdk-kms (~> 1)
25 | aws-sigv4 (~> 1.5)
26 | aws-sigv4 (1.12.1)
27 | aws-eventstream (~> 1, >= 1.0.2)
28 | babosa (1.0.4)
29 | base64 (0.3.0)
30 | bigdecimal (4.0.1)
31 | claide (1.0.3)
32 | colored (1.2)
33 | colored2 (3.1.2)
34 | commander (4.6.0)
35 | highline (~> 2.0.0)
36 | declarative (0.0.20)
37 | digest-crc (0.6.3)
38 | rake (>= 12.0.0, < 14.0.0)
39 | domain_name (0.5.20190701)
40 | unf (>= 0.0.5, < 1.0.0)
41 | dotenv (2.7.6)
42 | emoji_regex (3.2.2)
43 | excon (0.81.0)
44 | faraday (1.4.2)
45 | faraday-em_http (~> 1.0)
46 | faraday-em_synchrony (~> 1.0)
47 | faraday-excon (~> 1.1)
48 | faraday-net_http (~> 1.0)
49 | faraday-net_http_persistent (~> 1.1)
50 | multipart-post (>= 1.2, < 3)
51 | ruby2_keywords (>= 0.0.4)
52 | faraday-cookie_jar (0.0.7)
53 | faraday (>= 0.8.0)
54 | http-cookie (~> 1.0.0)
55 | faraday-em_http (1.0.0)
56 | faraday-em_synchrony (1.0.0)
57 | faraday-excon (1.1.0)
58 | faraday-net_http (1.0.1)
59 | faraday-net_http_persistent (1.1.0)
60 | faraday_middleware (1.0.0)
61 | faraday (~> 1.0)
62 | fastimage (2.2.3)
63 | fastlane (2.184.0)
64 | CFPropertyList (>= 2.3, < 4.0.0)
65 | addressable (>= 2.3, < 3.0.0)
66 | artifactory (~> 3.0)
67 | aws-sdk-s3 (~> 1.0)
68 | babosa (>= 1.0.3, < 2.0.0)
69 | bundler (>= 1.12.0, < 3.0.0)
70 | colored
71 | commander (~> 4.6)
72 | dotenv (>= 2.1.1, < 3.0.0)
73 | emoji_regex (>= 0.1, < 4.0)
74 | excon (>= 0.71.0, < 1.0.0)
75 | faraday (~> 1.0)
76 | faraday-cookie_jar (~> 0.0.6)
77 | faraday_middleware (~> 1.0)
78 | fastimage (>= 2.1.0, < 3.0.0)
79 | gh_inspector (>= 1.1.2, < 2.0.0)
80 | google-apis-androidpublisher_v3 (~> 0.1)
81 | google-apis-playcustomapp_v1 (~> 0.1)
82 | google-cloud-storage (~> 1.31)
83 | highline (~> 2.0)
84 | json (< 3.0.0)
85 | jwt (>= 2.1.0, < 3)
86 | mini_magick (>= 4.9.4, < 5.0.0)
87 | multipart-post (~> 2.0.0)
88 | naturally (~> 2.2)
89 | plist (>= 3.1.0, < 4.0.0)
90 | rubyzip (>= 2.0.0, < 3.0.0)
91 | security (= 0.1.3)
92 | simctl (~> 1.6.3)
93 | terminal-notifier (>= 2.0.0, < 3.0.0)
94 | terminal-table (>= 1.4.5, < 2.0.0)
95 | tty-screen (>= 0.6.3, < 1.0.0)
96 | tty-spinner (>= 0.8.0, < 1.0.0)
97 | word_wrap (~> 1.0.0)
98 | xcodeproj (>= 1.13.0, < 2.0.0)
99 | xcpretty (~> 0.3.0)
100 | xcpretty-travis-formatter (>= 0.0.3)
101 | gh_inspector (1.1.3)
102 | google-apis-androidpublisher_v3 (0.4.0)
103 | google-apis-core (~> 0.1)
104 | google-apis-core (0.3.0)
105 | addressable (~> 2.5, >= 2.5.1)
106 | googleauth (~> 0.14)
107 | httpclient (>= 2.8.1, < 3.0)
108 | mini_mime (~> 1.0)
109 | representable (~> 3.0)
110 | retriable (>= 2.0, < 4.0)
111 | rexml
112 | signet (~> 0.14)
113 | webrick
114 | google-apis-iamcredentials_v1 (0.4.0)
115 | google-apis-core (~> 0.1)
116 | google-apis-playcustomapp_v1 (0.3.0)
117 | google-apis-core (~> 0.1)
118 | google-apis-storage_v1 (0.4.0)
119 | google-apis-core (~> 0.1)
120 | google-cloud-core (1.6.0)
121 | google-cloud-env (~> 1.0)
122 | google-cloud-errors (~> 1.0)
123 | google-cloud-env (1.5.0)
124 | faraday (>= 0.17.3, < 2.0)
125 | google-cloud-errors (1.1.0)
126 | google-cloud-storage (1.31.1)
127 | addressable (~> 2.5)
128 | digest-crc (~> 0.4)
129 | google-apis-iamcredentials_v1 (~> 0.1)
130 | google-apis-storage_v1 (~> 0.1)
131 | google-cloud-core (~> 1.2)
132 | googleauth (~> 0.9)
133 | mini_mime (~> 1.0)
134 | googleauth (0.16.2)
135 | faraday (>= 0.17.3, < 2.0)
136 | jwt (>= 1.4, < 3.0)
137 | memoist (~> 0.16)
138 | multi_json (~> 1.11)
139 | os (>= 0.9, < 2.0)
140 | signet (~> 0.14)
141 | highline (2.0.3)
142 | http-cookie (1.0.3)
143 | domain_name (~> 0.5)
144 | httpclient (2.8.3)
145 | jmespath (1.6.2)
146 | json (2.5.1)
147 | jwt (2.2.3)
148 | logger (1.7.0)
149 | memoist (0.16.2)
150 | mini_magick (4.11.0)
151 | mini_mime (1.1.0)
152 | multi_json (1.15.0)
153 | multipart-post (2.0.0)
154 | nanaimo (0.3.0)
155 | naturally (2.2.1)
156 | os (1.1.1)
157 | plist (3.6.0)
158 | public_suffix (4.0.6)
159 | rake (13.0.3)
160 | representable (3.1.1)
161 | declarative (< 0.1.0)
162 | trailblazer-option (>= 0.1.1, < 0.2.0)
163 | uber (< 0.2.0)
164 | retriable (3.1.2)
165 | rexml (3.4.2)
166 | rouge (2.0.7)
167 | ruby2_keywords (0.0.4)
168 | rubyzip (2.3.0)
169 | security (0.1.3)
170 | signet (0.15.0)
171 | addressable (~> 2.3)
172 | faraday (>= 0.17.3, < 2.0)
173 | jwt (>= 1.5, < 3.0)
174 | multi_json (~> 1.10)
175 | simctl (1.6.8)
176 | CFPropertyList
177 | naturally
178 | terminal-notifier (2.0.0)
179 | terminal-table (1.8.0)
180 | unicode-display_width (~> 1.1, >= 1.1.1)
181 | trailblazer-option (0.1.1)
182 | tty-cursor (0.7.1)
183 | tty-screen (0.8.1)
184 | tty-spinner (0.9.3)
185 | tty-cursor (~> 0.7)
186 | uber (0.1.0)
187 | unf (0.1.4)
188 | unf_ext
189 | unf_ext (0.0.7.7)
190 | unf_ext (0.0.7.7-x64-mingw32)
191 | unicode-display_width (1.7.0)
192 | webrick (1.8.2)
193 | word_wrap (1.0.0)
194 | xcodeproj (1.19.0)
195 | CFPropertyList (>= 2.3.3, < 4.0)
196 | atomos (~> 0.1.3)
197 | claide (>= 1.0.2, < 2.0)
198 | colored2 (~> 3.1)
199 | nanaimo (~> 0.3.0)
200 | xcpretty (0.3.0)
201 | rouge (~> 2.0.7)
202 | xcpretty-travis-formatter (1.0.1)
203 | xcpretty (~> 0.2, >= 0.0.7)
204 |
205 | PLATFORMS
206 | x64-mingw32
207 | x86_64-linux
208 |
209 | DEPENDENCIES
210 | fastlane
211 |
212 | BUNDLED WITH
213 | 2.2.18
214 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
253 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/deeplviewer/webview/NestedScrollWebView.kt:
--------------------------------------------------------------------------------
1 | package com.example.deeplviewer.webview
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.util.Log
6 | import android.view.MotionEvent
7 | import android.view.VelocityTracker
8 | import android.view.ViewConfiguration
9 | import android.webkit.WebView
10 | import android.widget.OverScroller
11 | import androidx.core.view.*
12 | import kotlin.math.abs
13 |
14 | /**
15 | * Based on https://gist.github.com/alexmiragall/0c4c7163f7a17938518ce9794c4a5236
16 | *
17 | * A WebView which implements Nested Scrolling and can handle scrolling of child elements inside the WebView correctly.
18 | * The code was optimized to work with bottom sheets, but it might work for other NestedScroll operations as well.
19 | *
20 | */
21 | class NestedScrollWebView @JvmOverloads constructor(
22 | context: Context,
23 | attrs: AttributeSet? = null,
24 | defStyleAttr: Int = android.R.attr.webViewStyle
25 | ) :
26 | WebView(context, attrs, defStyleAttr), NestedScrollingChild {
27 |
28 | private val scrollOffset = IntArray(2)
29 | private val scrollConsumed = IntArray(2)
30 |
31 | private val nestedScrollingChildHelper: NestedScrollingChildHelper
32 | private var configuration: ViewConfiguration
33 | private var scroller: OverScroller
34 |
35 | private var lastMotionY = 0
36 | private var nestedYOffset = 0
37 | private var isBeingDragged = false
38 | private var isScrollingDown = false
39 |
40 | private var velocityTracker: VelocityTracker? = null
41 | private var activePointerId = INVALID_POINTER
42 |
43 | companion object {
44 | private const val INVALID_POINTER = -1
45 | private const val TAG = "NestedWebView"
46 | }
47 |
48 | init {
49 | overScrollMode = OVER_SCROLL_NEVER
50 | scroller = OverScroller(context)
51 | configuration = ViewConfiguration.get(context)
52 | nestedScrollingChildHelper = NestedScrollingChildHelper(this)
53 | isNestedScrollingEnabled = true
54 | isVerticalScrollBarEnabled = false
55 | isHorizontalScrollBarEnabled = false
56 | }
57 |
58 | override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
59 | val action = ev.action
60 | if (action == MotionEvent.ACTION_MOVE && isBeingDragged) {
61 | return true
62 | }
63 | when (action and MotionEvent.ACTION_MASK) {
64 | MotionEvent.ACTION_MOVE -> {
65 | activePointerId
66 | if (activePointerId == INVALID_POINTER) {
67 | return isBeingDragged
68 | } else {
69 | val pointerIndex = ev.findPointerIndex(activePointerId)
70 | if (pointerIndex == -1) {
71 | Log.e(
72 | TAG, "Invalid pointerId=" + activePointerId
73 | + " in onInterceptTouchEvent"
74 | )
75 | } else {
76 | val y = ev.getY(pointerIndex).toInt()
77 | if (lastMotionY == 0) {
78 | lastMotionY = y
79 | }
80 | val yDiff = y - lastMotionY
81 | if (abs(yDiff) > configuration.scaledTouchSlop
82 | && nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL == 0
83 | ) {
84 | lastMotionY = y
85 | initVelocityTrackerIfNotExists()
86 | velocityTracker!!.addMovement(ev)
87 | nestedYOffset = 0
88 | parent?.requestDisallowInterceptTouchEvent(true)
89 | }
90 | }
91 | }
92 | }
93 | MotionEvent.ACTION_DOWN -> {
94 | lastMotionY = ev.y.toInt()
95 | activePointerId = ev.getPointerId(0)
96 | initOrResetVelocityTracker()
97 | velocityTracker!!.addMovement(ev)
98 | scroller.computeScrollOffset()
99 | if (scroller.isFinished) {
100 | isBeingDragged = false
101 | }
102 | }
103 | MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
104 | isBeingDragged = false
105 | activePointerId = INVALID_POINTER
106 | recycleVelocityTracker()
107 | if (scroller.springBack(scrollX, scrollY, 0, 0, 0, computeVerticalScrollRange())) {
108 | ViewCompat.postInvalidateOnAnimation(this)
109 | }
110 | stopNestedScroll()
111 | }
112 | MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev)
113 | }
114 | return isBeingDragged
115 | }
116 |
117 | override fun onTouchEvent(ev: MotionEvent): Boolean {
118 | initVelocityTrackerIfNotExists()
119 | val vtev = MotionEvent.obtain(ev)
120 | val actionMasked = MotionEventCompat.getActionMasked(ev)
121 | if (actionMasked == MotionEvent.ACTION_DOWN) {
122 | nestedYOffset = 0
123 | }
124 | vtev.offsetLocation(0f, nestedYOffset.toFloat())
125 | when (actionMasked) {
126 | MotionEvent.ACTION_DOWN -> {
127 | if (!scroller.isFinished) {
128 | val parent = parent
129 | parent?.requestDisallowInterceptTouchEvent(true)
130 | }
131 | if (!scroller.isFinished) {
132 | scroller.abortAnimation()
133 | }
134 | lastMotionY = ev.y.toInt()
135 | activePointerId = ev.getPointerId(0)
136 | }
137 | MotionEvent.ACTION_MOVE -> {
138 | val activePointerIndex = ev.findPointerIndex(activePointerId)
139 | if (activePointerIndex == -1) {
140 | Log.e(
141 | TAG,
142 | "Invalid pointerId=$activePointerId in onTouchEvent"
143 | )
144 | } else {
145 | val y = ev.getY(activePointerIndex).toInt()
146 | if (lastMotionY == 0) {
147 | lastMotionY = y
148 | }
149 | var deltaY = lastMotionY - y
150 | if (deltaY < -configuration.scaledTouchSlop) {
151 | if (isScrollingDown) {
152 | endNestedDrag()
153 | }
154 | isScrollingDown = false
155 | }
156 | if (dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) {
157 | deltaY -= scrollConsumed[1]
158 | vtev.offsetLocation(0f, scrollOffset[1].toFloat())
159 | nestedYOffset += scrollOffset[1]
160 | }
161 | if (!isBeingDragged && abs(deltaY) > configuration.scaledTouchSlop) {
162 | parent?.requestDisallowInterceptTouchEvent(true)
163 | if (deltaY > 0) {
164 | deltaY -= configuration.scaledTouchSlop
165 | startNestedDrag()
166 | isScrollingDown = true
167 | } else {
168 | deltaY += configuration.scaledTouchSlop
169 | }
170 | }
171 | if (isBeingDragged) {
172 | lastMotionY = y - scrollOffset[1]
173 | val oldY = scrollY
174 | val scrolledDeltaY = scrollY - oldY
175 | val unconsumedY = deltaY - scrolledDeltaY
176 | if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, scrollOffset)) {
177 | lastMotionY -= scrollOffset[1]
178 | vtev.offsetLocation(0f, scrollOffset[1].toFloat())
179 | nestedYOffset += scrollOffset[1]
180 | }
181 | }
182 | }
183 | }
184 | MotionEvent.ACTION_UP -> {
185 | if (isBeingDragged) {
186 | velocityTracker!!.computeCurrentVelocity(
187 | 1000,
188 | configuration.scaledMaximumFlingVelocity.toFloat()
189 | )
190 | val initialVelocity = VelocityTrackerCompat.getYVelocity(
191 | velocityTracker,
192 | activePointerId
193 | ).toInt()
194 | if (abs(initialVelocity) > configuration.scaledMinimumFlingVelocity) {
195 | flingWithNestedDispatch(-initialVelocity)
196 | } else if (scroller.springBack(
197 | scrollX, scrollY, 0, 0, 0,
198 | computeVerticalScrollRange()
199 | )
200 | ) {
201 | ViewCompat.postInvalidateOnAnimation(this)
202 | }
203 | }
204 | activePointerId = INVALID_POINTER
205 | endNestedDrag()
206 | }
207 | MotionEvent.ACTION_CANCEL -> {
208 | if (isBeingDragged && childCount > 0) {
209 | if (scroller.springBack(
210 | scrollX, scrollY, 0, 0, 0,
211 | computeVerticalScrollRange()
212 | )
213 | ) {
214 | ViewCompat.postInvalidateOnAnimation(this)
215 | }
216 | }
217 | activePointerId = INVALID_POINTER
218 | endNestedDrag()
219 | }
220 | MotionEventCompat.ACTION_POINTER_DOWN -> {
221 | val index = MotionEventCompat.getActionIndex(ev)
222 | lastMotionY = ev.getY(index).toInt()
223 | activePointerId = ev.getPointerId(index)
224 | }
225 | MotionEventCompat.ACTION_POINTER_UP -> {
226 | onSecondaryPointerUp(ev)
227 | lastMotionY = ev.getY(ev.findPointerIndex(activePointerId)).toInt()
228 | }
229 | }
230 | velocityTracker?.addMovement(vtev)
231 | vtev.recycle()
232 | return super.onTouchEvent(ev)
233 | }
234 |
235 | override fun onOverScrolled(scrollX: Int, scrollY: Int, clampedX: Boolean, clampedY: Boolean) {
236 | super.onOverScrolled(scrollX, scrollY, clampedX, clampedY)
237 |
238 | if (clampedY && !isBeingDragged) {
239 | parent?.requestDisallowInterceptTouchEvent(true)
240 | initOrResetVelocityTracker()
241 | isScrollingDown = false
242 | lastMotionY = 0
243 | nestedYOffset = 0
244 | startNestedDrag()
245 | }
246 | }
247 |
248 | private fun startNestedDrag() {
249 | isBeingDragged = true
250 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
251 | }
252 |
253 | private fun endNestedDrag() {
254 | isBeingDragged = false
255 | recycleVelocityTracker()
256 | stopNestedScroll()
257 | }
258 |
259 | private fun onSecondaryPointerUp(ev: MotionEvent) {
260 | val pointerIndex = (ev.action and MotionEvent.ACTION_POINTER_INDEX_MASK
261 | shr MotionEvent.ACTION_POINTER_INDEX_SHIFT)
262 | val pointerId = ev.getPointerId(pointerIndex)
263 | if (pointerId == activePointerId) {
264 | val newPointerIndex = if (pointerIndex == 0) 1 else 0
265 | lastMotionY = ev.getY(newPointerIndex).toInt()
266 | activePointerId = ev.getPointerId(newPointerIndex)
267 | velocityTracker?.clear()
268 | }
269 | }
270 |
271 | private fun initOrResetVelocityTracker() {
272 | if (velocityTracker == null) {
273 | velocityTracker = VelocityTracker.obtain()
274 | } else {
275 | velocityTracker!!.clear()
276 | }
277 | }
278 |
279 | private fun initVelocityTrackerIfNotExists() {
280 | if (velocityTracker == null) {
281 | velocityTracker = VelocityTracker.obtain()
282 | }
283 | }
284 |
285 | private fun recycleVelocityTracker() {
286 | velocityTracker?.recycle()
287 | velocityTracker = null
288 | }
289 |
290 | private fun flingWithNestedDispatch(velocityY: Int) {
291 | val scrollY = scrollY
292 | val canFling = ((scrollY > 0 || velocityY > 0)
293 | && (scrollY < computeVerticalScrollRange() || velocityY < 0))
294 | if (!dispatchNestedPreFling(0f, velocityY.toFloat())) {
295 | dispatchNestedFling(0f, velocityY.toFloat(), canFling)
296 | if (canFling) {
297 | fling(velocityY)
298 | }
299 | }
300 | }
301 |
302 | private fun fling(velocityY: Int) {
303 | if (childCount > 0) {
304 | val height = height - paddingBottom - paddingTop
305 | val bottom = getChildAt(0).height
306 | scroller.fling(
307 | scrollX, scrollY, 0, velocityY, 0, 0, 0,
308 | (bottom - height).coerceAtLeast(0), 0, height / 2
309 | )
310 | ViewCompat.postInvalidateOnAnimation(this)
311 | }
312 | }
313 |
314 | override fun isNestedScrollingEnabled(): Boolean {
315 | return nestedScrollingChildHelper.isNestedScrollingEnabled
316 | }
317 |
318 | override fun setNestedScrollingEnabled(enabled: Boolean) {
319 | nestedScrollingChildHelper.isNestedScrollingEnabled = enabled
320 | }
321 |
322 | override fun startNestedScroll(axes: Int): Boolean {
323 | return nestedScrollingChildHelper.startNestedScroll(axes)
324 | }
325 |
326 | override fun stopNestedScroll() {
327 | nestedScrollingChildHelper.stopNestedScroll()
328 | }
329 |
330 | override fun hasNestedScrollingParent(): Boolean {
331 | return nestedScrollingChildHelper.hasNestedScrollingParent()
332 | }
333 |
334 | override fun dispatchNestedScroll(
335 | dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int,
336 | offsetInWindow: IntArray?
337 | ): Boolean {
338 | return nestedScrollingChildHelper.dispatchNestedScroll(
339 | dxConsumed,
340 | dyConsumed,
341 | dxUnconsumed,
342 | dyUnconsumed,
343 | offsetInWindow
344 | )
345 | }
346 |
347 | override fun dispatchNestedPreScroll(
348 | dx: Int,
349 | dy: Int,
350 | consumed: IntArray?,
351 | offsetInWindow: IntArray?
352 | ): Boolean {
353 | return nestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
354 | }
355 |
356 | override fun dispatchNestedFling(
357 | velocityX: Float,
358 | velocityY: Float,
359 | consumed: Boolean
360 | ): Boolean {
361 | return nestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed)
362 | }
363 |
364 | override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
365 | return nestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY)
366 | }
367 |
368 | override fun getNestedScrollAxes(): Int {
369 | return ViewCompat.SCROLL_AXIS_NONE
370 | }
371 | }
372 |
--------------------------------------------------------------------------------