├── .github ├── ISSUE_TEMPLATE.md └── assets │ └── direct-apk-download.png ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── app ├── build.gradle ├── debug.keystore ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── style.css │ ├── ic_launcher-web.png │ ├── java │ │ └── com │ │ │ ├── github │ │ │ └── takahirom │ │ │ │ └── webview_in_coodinator_layout │ │ │ │ └── NestedWebView.java │ │ │ └── jtmcn │ │ │ └── archwiki │ │ │ └── viewer │ │ │ ├── ArchWikiViewerApp.kt │ │ │ ├── Constants.kt │ │ │ ├── MainActivity.kt │ │ │ ├── PreferencesActivity.kt │ │ │ ├── SearchResultsAdapter.kt │ │ │ ├── WikiClient.kt │ │ │ ├── WikiView.kt │ │ │ ├── data │ │ │ ├── SearchResult.kt │ │ │ ├── SearchResultsBuilder.kt │ │ │ ├── WikiPage.kt │ │ │ └── WikiPageBuilder.kt │ │ │ ├── tasks │ │ │ ├── Fetch.kt │ │ │ └── FetchUrl.kt │ │ │ └── utils │ │ │ ├── AndroidUtils.kt │ │ │ ├── NetworkUtils.kt │ │ │ └── SettingsUtils.kt │ ├── play │ │ ├── contact-email.txt │ │ ├── contact-website.txt │ │ ├── default-language.txt │ │ ├── listings │ │ │ └── en-US │ │ │ │ ├── full-description.txt │ │ │ │ ├── graphics │ │ │ │ ├── feature-graphic │ │ │ │ │ └── 0.png │ │ │ │ ├── icon │ │ │ │ │ └── 0.png │ │ │ │ └── phone-screenshots │ │ │ │ │ ├── 0.png │ │ │ │ │ ├── 1.png │ │ │ │ │ ├── 2.png │ │ │ │ │ ├── 3.png │ │ │ │ │ └── 4.png │ │ │ │ ├── short-description.txt │ │ │ │ └── title.txt │ │ └── release-notes │ │ │ └── en-US │ │ │ ├── beta.txt │ │ │ └── default.txt │ └── res │ │ ├── drawable │ │ ├── ic_launcher.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_search_white_24dp.xml │ │ └── ic_share_white_24dp.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_preferences.xml │ │ ├── search_suggestions_list_item.xml │ │ └── toolbar.xml │ │ ├── menu │ │ └── menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-es │ │ └── strings.xml │ │ ├── values-fr │ │ └── strings.xml │ │ ├── values-it │ │ └── strings.xml │ │ ├── values-iw │ │ └── strings.xml │ │ ├── values-pt-rBR │ │ └── strings.xml │ │ ├── values-sk │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── preferences.xml │ │ └── searchable.xml │ └── test │ └── java │ └── com │ └── jtmcn │ └── archwiki │ └── viewer │ └── data │ ├── SearchResultsBuilderTest.kt │ └── WikiPageBuilderTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── playstore ├── archwiki_feature_graphic.svg └── ic_launcher.svg ├── settings.gradle └── upload_config.tar.enc /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 3 | 4 | I have: 5 | - [ ] searched open and closed issues for duplicates 6 | - [ ] provided a reproducible result if applicable 7 | 8 | ---------------------------------------- 9 | 10 | 11 | ### Bug description 12 | Describe here the issue that you are experiencing. 13 | 14 | ### Steps to reproduce 15 | - using hyphens as bullet points 16 | - list the steps 17 | - that reproduce the bug 18 | 19 | **Actual result:** Describe here what happens after you run the steps above (i.e. the buggy behaviour) 20 | **Expected result:** Describe here what should happen after you run the steps above (i.e. what would be the correct behaviour) 21 | 22 | ### Device info 23 | 24 | **Device:** Manufacturer Model XVI 25 | **Android version:** 0.0.0 26 | **App version:** 0.0.0 27 | -------------------------------------------------------------------------------- /.github/assets/direct-apk-download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/.github/assets/direct-apk-download.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | *.aab 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # IntelliJ 37 | *.iml 38 | .idea/ 39 | 40 | # Keystore files 41 | # Uncomment the following lines if you do not want to check your keystore files in. 42 | #*.jks 43 | #*.keystore 44 | 45 | # External native build folder generated in Android Studio 2.2 and later 46 | .externalNativeBuild 47 | 48 | # Google Services (e.g. APIs or Firebase) 49 | google-services.json 50 | 51 | # Freeline 52 | freeline.py 53 | freeline/ 54 | freeline_project_description.json 55 | 56 | # fastlane 57 | fastlane/report.xml 58 | fastlane/Preview.html 59 | fastlane/screenshots 60 | fastlane/test_output 61 | fastlane/readme.md 62 | 63 | # Version control 64 | vcs.xml 65 | 66 | # lint 67 | lint/intermediates/ 68 | lint/generated/ 69 | lint/outputs/ 70 | lint/tmp/ 71 | # lint/reports/ 72 | 73 | 74 | # Deploy files 75 | app/upload.keystore 76 | app/upload.json 77 | upload_config.tar 78 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: oraclejdk8 3 | sudo: false 4 | env: 5 | global: 6 | - ANDROID_API=29 7 | - EMULATOR_API=21 8 | - ANDROID_BUILD_TOOLS=29.0.2 9 | android: 10 | components: 11 | - build-tools-$ANDROID_BUILD_TOOLS 12 | - android-$ANDROID_API 13 | - android-$EMULATOR_API 14 | - extra-android-support 15 | - extra-google-google_play_services 16 | - extra-google-m2repository 17 | - extra-android-m2repository 18 | - sys-img-armeabi-v7a-android-$EMULATOR_API 19 | 20 | before_install: 21 | - if [ -n "$TRAVIS_TAG" ]; then echo "Decrypting upload config"; openssl aes-256-cbc 22 | -K $encrypted_da56e498cb22_key -iv $encrypted_da56e498cb22_iv -in upload_config.tar.enc 23 | -out upload_config.tar -d; echo "Successfully decrypted files"; tar xvf upload_config.tar; 24 | mv upload_config/upload.json app/upload.json; mv upload_config/upload.keystore app/upload.keystore; 25 | fi 26 | - "./gradlew lint" 27 | 28 | before_script: 29 | - echo no | android create avd --force -n test -t android-$EMULATOR_API --abi armeabi-v7a 30 | - emulator -avd test -no-skin -no-audio -no-window & 31 | - android-wait-for-emulator 32 | - adb shell input keyevent 82 & 33 | 34 | script: "./gradlew build test connectedAndroidTest" 35 | 36 | before_cache: 37 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 38 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 39 | cache: 40 | directories: 41 | - "$HOME/.gradle/caches/" 42 | - "$HOME/.gradle/wrapper/" 43 | - "$HOME/.android/build-cache" 44 | 45 | before_deploy: 46 | - cd app/build/outputs/apk/release/; ls -la; md5sum archwiki-viewer-*.apk > md5sum.txt; 47 | sha1sum archwiki-viewer-*.apk > sha1sum.txt; cd - 48 | 49 | deploy: 50 | skip_cleanup: true 51 | provider: releases 52 | api_key: 53 | secure: F/LqVUBz5fMT28Av5NT3uGUYMxMepkk1XjL7XD/rtlwh1ApJjQDdwEBDSuFiXIqMv5ru8Gvo6u8HsiEJurnuiJ16wkTdyGDvGQffGmm2OIAko6fnlRpKvboN03eEuHElONqHzX792QMCUcSM6YnkSPJdydxhza1BRHnYsECLPcGQzp6CFNKCkVOsfX0XO7dSuZwVKZJufRDLbInaJgAYuKRjeG5Qj+3tM6WDUqOaVroSxeN8cfW14OSShTgXYYtZBYEcrt3p75/9qftMMPVAd5599/voFYJZHMzvZ321dEB84FJX1woY7MJp/tJMdfTVYuUEtYY9XwoR4H/DW+dvwlFFz+v/bNyCoV30KIZm/L7ohNeH5zbLA8bAbHFOcsOW3gXMxgv4gCquenh5/9xDUOIX8CQmjwcvmNzNcx7SxoIuOe2ghttVBziNQPL6D+Xbnd+W7uzlLLleJGnhVomkomFYhwy29a+/fM1gjKRJ7H/VruqCzfWMoXDektSoD3BLAR5Ckm9WRvEPQW5R1Q8gk4Bt13if43GwtI83qh8UQjleJLiVRm6e7HsGqoLnjCoN6gRDx0oo+h3S2vrp4QBe9vOZuCurWXrHDdf5CtuwSAFTpYoT9EF8IfOZ3sXrwRSdtsJaiactj1h2cUK2FJ0Rgy6gxHqeT1sh8HeRetQJZAQ= 54 | file_glob: true 55 | name: $TRAVIS_TAG 56 | file: 57 | - app/build/outputs/apk/release/archwiki-viewer-*.apk 58 | - app/build/outputs/apk/release/md5sum.txt 59 | - app/build/outputs/apk/release/sha1sum.txt 60 | on: 61 | repo: kevinhinterlong/archwiki-viewer 62 | tags: true 63 | 64 | after_deploy: 65 | - echo "Checking if tag matches v.X.Y.Z" 66 | - if [[ $TRAVIS_TAG =~ v[0-9]+\.[0-9]+\.[0-9]+ ]] ; then ./gradlew publishRelease; 67 | fi 68 | 69 | notifications: 70 | webhooks: 71 | on_success: change 72 | on_failure: always 73 | on_start: never 74 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 kevinhinterlong 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ArchWiki Viewer 2 | [![Build Status](https://api.travis-ci.org/kevinhinterlong/archwiki-viewer.svg?branch=master)](https://travis-ci.org/kevinhinterlong/archwiki-viewer) [![GitHub release](https://img.shields.io/github/release/kevinhinterlong/archwiki-viewer.svg)](https://github.com/kevinhinterlong/archwiki-viewer/releases) 3 | =============== 4 | A simple viewer for the Arch Linux Wiki. Page content is formatted for optimal mobile viewing. 5 | 6 | [Get it on Google Play](https://play.google.com/store/apps/details?id=com.jtmcn.archwiki.viewer) [Get it on F-Droid](https://f-droid.org/repository/browse/?fdid=com.jtmcn.archwiki.viewer) [Direct apk download](https://github.com/kevinhinterlong/SwishTicker/releases/latest) 7 | 8 | ## Screenshots 9 | 10 | 11 | 12 | ## Contributions 13 | All contributions are welcome, don't forget to ask if you need help. 14 | 15 | Comments and tests are highly encouraged. 16 | 17 | ## License 18 | This project is licensed under the Apache License, Version 2.0 19 | 20 | Copyright 2019 kevinhinterlong 21 | 22 | See [LICENSE.md](LICENSE.md) 23 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'com.github.triplet.play' version '2.4.2' 4 | } 5 | apply plugin: 'kotlin-android' 6 | apply plugin: 'kotlin-android-extensions' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | android { 13 | applicationVariants.all { variant -> 14 | variant.outputs.all { 15 | outputFileName = "archwiki-viewer-v${variant.versionName}.apk" 16 | } 17 | } 18 | 19 | compileSdkVersion 29 20 | buildToolsVersion '29.0.2' 21 | defaultConfig { 22 | applicationId "com.jtmcn.archwiki.viewer" 23 | minSdkVersion 21 24 | targetSdkVersion 29 25 | versionCode 15 26 | versionName "1.0.14" 27 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 28 | } 29 | 30 | signingConfigs { 31 | debug { 32 | storeFile file('debug.keystore') 33 | storePassword 'archwiki-viewer' 34 | keyAlias 'archwiki-viewer' 35 | keyPassword 'archwiki-viewer' 36 | } 37 | if (file('upload.keystore').exists()) { 38 | upload { 39 | storeFile file('upload.keystore') 40 | storePassword System.getenv('ARCHWIKI_VIEWER_UPLOAD_STORE_PASSWORD') 41 | keyAlias 'awvalias' 42 | keyPassword System.getenv('ARCHWIKI_VIEWER_UPLOAD_KEY_PASSWORD') 43 | } 44 | } 45 | } 46 | 47 | buildTypes { 48 | debug { 49 | signingConfig signingConfigs.debug 50 | } 51 | release { 52 | if (file('upload.keystore').exists()) { 53 | signingConfig signingConfigs.upload 54 | } else { 55 | signingConfig signingConfigs.debug 56 | } 57 | minifyEnabled true 58 | shrinkResources true 59 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 60 | } 61 | } 62 | } 63 | 64 | dependencies { 65 | implementation fileTree(include: ['*.jar'], dir: 'libs') 66 | testImplementation 'junit:junit:4.12' 67 | androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { 68 | exclude group: 'com.android.support', module: 'support-annotations' 69 | }) 70 | 71 | implementation 'com.google.code.gson:gson:2.8.5' 72 | implementation 'com.jakewharton.timber:timber:4.7.1' 73 | 74 | implementation 'com.squareup.okhttp3:okhttp:3.11.0' 75 | 76 | implementation 'com.google.android.material:material:1.0.0' 77 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 78 | implementation 'androidx.appcompat:appcompat:1.1.0' 79 | implementation 'androidx.preference:preference:1.1.0' 80 | implementation 'androidx.core:core-ktx:1.1.0' 81 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 82 | } 83 | 84 | play { 85 | serviceAccountCredentials = file("upload.json") 86 | track = "beta" 87 | } 88 | -------------------------------------------------------------------------------- /app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/debug.keystore -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can edit the include path and order by changing the proguardFiles 3 | # directive in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # Add any project specific keep options here: 9 | 10 | # If your project uses WebView with JS, uncomment the following 11 | # and specify the fully qualified class name to the JavaScript interface 12 | # class: 13 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 14 | # public *; 15 | #}-keepnames class org.glassfish.** { *; } 16 | 17 | -keep class android.support.v7.internal.** { *; } 18 | -keep interface android.support.v7.internal.** { *; } 19 | -keep class android.support.v7.** { *; } 20 | -keep interface android.support.v7.** { *; } 21 | 22 | # OkHttp 23 | # JSR 305 annotations are for embedding nullability information. 24 | -dontwarn javax.annotation.** 25 | 26 | # A resource is loaded with a relative path so the package of this class must be preserved. 27 | -keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase 28 | 29 | # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. 30 | -dontwarn org.codehaus.mojo.animal_sniffer.* 31 | 32 | # OkHttp platform used only on JVM and when Conscrypt dependency is available. 33 | -dontwarn okhttp3.internal.platform.ConscryptPlatform 34 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 42 | 43 | 47 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/assets/style.css: -------------------------------------------------------------------------------- 1 | .mw-content-ltr { 2 | direction: ltr 3 | } 4 | 5 | .mw-content-rtl { 6 | direction: rtl 7 | } 8 | 9 | .sitedir-ltr textarea,.sitedir-ltr input { 10 | direction: ltr 11 | } 12 | 13 | .sitedir-rtl textarea,.sitedir-rtl input { 14 | direction: rtl 15 | } 16 | 17 | input[type="submit"],input[type="button"],input[type="reset"],input[type="file"] 18 | { 19 | direction: ltr 20 | } 21 | 22 | textarea[dir="ltr"],input[dir="ltr"] { 23 | direction: ltr 24 | } 25 | 26 | textarea[dir="rtl"],input[dir="rtl"] { 27 | direction: rtl 28 | } 29 | 30 | abbr,acronym,.explain { 31 | border-bottom: 1px dotted; 32 | cursor: help 33 | } 34 | 35 | .mw-plusminus-pos { 36 | color: #006400; 37 | } 38 | 39 | .mw-plusminus-neg { 40 | color: #8b0000; 41 | } 42 | 43 | .mw-plusminus-null { 44 | color: #aaa; 45 | } 46 | 47 | .allpagesredirect,.redirect-in-category,.watchlistredir { 48 | font-style: italic 49 | } 50 | 51 | span.comment { 52 | font-style: italic 53 | } 54 | 55 | span.changedby { 56 | font-size: 95% 57 | } 58 | 59 | .texvc { 60 | direction: ltr; 61 | unicode-bidi: embed 62 | } 63 | 64 | img.tex { 65 | vertical-align: middle 66 | } 67 | 68 | span.texhtml { 69 | font-family: serif 70 | } 71 | 72 | #wikiPreview.ontop { 73 | margin-bottom: 1em 74 | } 75 | 76 | #editform,#toolbar,#wpTextbox1 { 77 | clear: both 78 | } 79 | 80 | #toolbar img { 81 | cursor: pointer 82 | } 83 | 84 | .editsection { 85 | float: right; 86 | margin-left: 5px 87 | } 88 | 89 | .mw-content-ltr .editsection,.mw-content-rtl .mw-content-ltr .editsection 90 | { 91 | float: right 92 | } 93 | 94 | .mw-content-rtl .editsection,.mw-content-ltr .mw-content-rtl .editsection 95 | { 96 | float: left 97 | } 98 | 99 | div.mw-filepage-resolutioninfo { 100 | font-size: smaller 101 | } 102 | 103 | td.mw-label { 104 | text-align: right 105 | } 106 | 107 | td.mw-input { 108 | text-align: left 109 | } 110 | 111 | td.mw-submit { 112 | text-align: left 113 | } 114 | 115 | td.mw-label { 116 | vertical-align: top 117 | } 118 | 119 | .prefsection td.mw-label { 120 | width: 20% 121 | } 122 | 123 | .prefsection table { 124 | width: 100% 125 | } 126 | 127 | td.mw-submit { 128 | white-space: nowrap 129 | } 130 | 131 | table.mw-htmlform-nolabel td.mw-label { 132 | width: 1px 133 | } 134 | 135 | tr.mw-htmlform-vertical-label td.mw-label { 136 | text-align: left !important 137 | } 138 | 139 | .mw-htmlform-invalid-input td.mw-input input { 140 | border-color: red 141 | } 142 | 143 | .mw-htmlform-flatlist div.mw-htmlform-flatlist-item { 144 | display: inline; 145 | margin-right: 1em; 146 | white-space: nowrap 147 | } 148 | 149 | input#wpSummary { 150 | width: 80% 151 | } 152 | 153 | .thumbcaption { 154 | text-align: left 155 | } 156 | 157 | .magnify { 158 | float: right 159 | } 160 | 161 | p.mw-ipb-conveniencelinks,p.mw-protect-editreasons,p.mw-filedelete-editreasons,p.mw-delete-editreasons,p.mw-revdel-editreasons 162 | { 163 | font-size: 90%; 164 | text-align: right 165 | } 166 | 167 | .autocomment { 168 | color: gray 169 | } 170 | 171 | .mw-history-revisiondelete-button,#mw-fileduplicatesearch-icon { 172 | float: right 173 | } 174 | 175 | .newpage,.minoredit,.botedit { 176 | font-weight: bold 177 | } 178 | 179 | #shared-image-dup,#shared-image-conflict { 180 | font-style: italic 181 | } 182 | 183 | div.mw-warning-with-logexcerpt { 184 | padding: 3px; 185 | margin-bottom: 3px; 186 | border: 2px solid #2F6FAB; 187 | clear: both 188 | } 189 | 190 | div.mw-warning-with-logexcerpt ul li { 191 | font-size: 90% 192 | } 193 | 194 | span.mw-revdelundel-link,strong.mw-revdelundel-link { 195 | font-size: 90% 196 | } 197 | 198 | span.mw-revdelundel-hidden,input.mw-revdelundel-hidden { 199 | visibility: hidden 200 | } 201 | 202 | td.mw-revdel-checkbox,th.mw-revdel-checkbox { 203 | padding-right: 10px; 204 | text-align: center 205 | } 206 | 207 | .plainlinks a { 208 | background: none !important; 209 | padding: 0 !important 210 | } 211 | 212 | .rtl a.external.free,.rtl a.external.autonumber { 213 | direction: ltr; 214 | unicode-bidi: embed 215 | } 216 | 217 | table.wikitable { 218 | margin: 1em 1em 1em 0; 219 | background-color: #f9f9f9; 220 | border: 1px #aaa solid; 221 | border-collapse: collapse; 222 | color: black; 223 | font-size: 90% 224 | } 225 | 226 | table.wikitable> ; tr> ; th,table.wikitable> ; tr> ; td,table.wikitable> 227 | ; *> ; tr> ; th,table.wikitable> ; *> ; tr> ; td { 228 | border: 1px #aaa solid; 229 | padding: 0.2em 230 | } 231 | 232 | table.wikitable> ; tr> ; th,table.wikitable> ; *> ; tr> ; th { 233 | background-color: #f2f2f2; 234 | text-align: center 235 | } 236 | 237 | table.wikitable> ; caption { 238 | font-weight: bold 239 | } 240 | 241 | table.collapsed tr.collapsable { 242 | display: none 243 | } 244 | 245 | .success { 246 | color: green; 247 | font-size: larger 248 | } 249 | 250 | .warning { 251 | color: #FFA500; 252 | font-size: larger 253 | } 254 | 255 | .error { 256 | color: red; 257 | font-size: larger 258 | } 259 | 260 | .errorbox,.warningbox,.successbox { 261 | font-size: larger; 262 | border: 2px solid; 263 | padding: .5em 1em; 264 | float: left; 265 | margin-bottom: 2em; 266 | color: #000 267 | } 268 | 269 | .errorbox { 270 | border-color: red; 271 | background-color: #fff2f2 272 | } 273 | 274 | .warningbox { 275 | border-color: #FF8C00; 276 | background-color: #FFFFC0 277 | } 278 | 279 | .successbox { 280 | border-color: green; 281 | background-color: #dfd 282 | } 283 | 284 | .errorbox h2,.warningbox h2,.successbox h2 { 285 | font-size: 1em; 286 | font-weight: bold; 287 | display: inline; 288 | margin: 0 .5em 0 0; 289 | border: none 290 | } 291 | 292 | .mw-infobox { 293 | border: 2px solid #ff7f00; 294 | margin: 0.5em; 295 | clear: left; 296 | overflow: hidden 297 | } 298 | 299 | .mw-infobox-left { 300 | margin: 7px; 301 | float: left; 302 | width: 35px 303 | } 304 | 305 | .mw-infobox-right { 306 | margin: 0.5em 0.5em 0.5em 49px 307 | } 308 | 309 | .previewnote { 310 | color: #c00; 311 | margin-bottom: 1em 312 | } 313 | 314 | .previewnote p { 315 | text-indent: 3em; 316 | margin: 0.8em 0 317 | } 318 | 319 | .visualClear { 320 | clear: both 321 | } 322 | 323 | #mw_trackbacks { 324 | border: solid 1px #bbbbff; 325 | background-color: #eeeeff; 326 | padding: 0.2em 327 | } 328 | 329 | .mw-datatable { 330 | border-collapse: collapse 331 | } 332 | 333 | .mw-datatable,.mw-datatable td,.mw-datatable th { 334 | border: 1px solid #aaaaaa; 335 | padding: 0 0.15em 0 0.15em 336 | } 337 | 338 | .mw-datatable th { 339 | background-color: #ddddff 340 | } 341 | 342 | .mw-datatable td { 343 | background-color: #ffffff 344 | } 345 | 346 | .mw-datatable tr:hover td { 347 | background-color: #eeeeff 348 | } 349 | 350 | .TablePager { 351 | min-width: 80% 352 | } 353 | 354 | .TablePager_nav { 355 | margin: 0 auto 356 | } 357 | 358 | .TablePager_nav td { 359 | padding: 3px; 360 | text-align: center 361 | } 362 | 363 | .TablePager_nav a { 364 | text-decoration: none 365 | } 366 | 367 | .imagelist td,.imagelist th { 368 | white-space: nowrap 369 | } 370 | 371 | .imagelist .TablePager_col_links { 372 | background-color: #eeeeff 373 | } 374 | 375 | .imagelist .TablePager_col_img_description { 376 | white-space: normal 377 | } 378 | 379 | .imagelist th.TablePager_sort { 380 | background-color: #ccccff 381 | } 382 | 383 | ul#filetoc { 384 | text-align: center; 385 | border: 1px solid #aaaaaa; 386 | background-color: #f9f9f9; 387 | padding: 5px; 388 | font-size: 95%; 389 | margin-bottom: 0.5em; 390 | margin-left: 0; 391 | margin-right: 0 392 | } 393 | 394 | #filetoc li { 395 | display: inline; 396 | list-style-type: none; 397 | padding-right: 2em 398 | } 399 | 400 | table.mw_metadata { 401 | font-size: 0.8em; 402 | margin-left: 0.5em; 403 | margin-bottom: 0.5em; 404 | width: 400px 405 | } 406 | 407 | table.mw_metadata caption { 408 | font-weight: bold 409 | } 410 | 411 | table.mw_metadata th { 412 | font-weight: normal 413 | } 414 | 415 | table.mw_metadata td { 416 | padding: 0.1em 417 | } 418 | 419 | table.mw_metadata { 420 | border: none; 421 | border-collapse: collapse 422 | } 423 | 424 | table.mw_metadata td,table.mw_metadata th { 425 | text-align: center; 426 | border: 1px solid #aaaaaa; 427 | padding-left: 5px; 428 | padding-right: 5px 429 | } 430 | 431 | table.mw_metadata th { 432 | background-color: #f9f9f9 433 | } 434 | 435 | table.mw_metadata td { 436 | background-color: #fcfcfc 437 | } 438 | 439 | table.mw_metadata ul.metadata-langlist { 440 | list-style-type: none; 441 | list-style-image: none; 442 | padding-right: 5px; 443 | padding-left: 5px; 444 | margin: 0 445 | } 446 | 447 | .mw-content-ltr ul,.mw-content-rtl .mw-content-ltr ul { 448 | word-wrap: break-word; 449 | margin: 0.3em 0 0 1.6em; 450 | padding: 0 451 | } 452 | 453 | .mw-content-rtl ul,.mw-content-ltr .mw-content-rtl ul { 454 | word-wrap: break-word; 455 | margin: 0.3em 1.6em 0 0; 456 | padding: 0 457 | } 458 | 459 | .mw-content-ltr ol,.mw-content-rtl .mw-content-ltr ol { 460 | margin: 0.3em 0 0 3.2em; 461 | padding: 0 462 | } 463 | 464 | .mw-content-rtl ol,.mw-content-ltr .mw-content-rtl ol { 465 | margin: 0.3em 3.2em 0 0; 466 | padding: 0 467 | } 468 | 469 | .mw-content-ltr dd,.mw-content-rtl .mw-content-ltr dd { 470 | margin-left: 1.6em; 471 | margin-right: 0 472 | } 473 | 474 | .mw-content-rtl dd,.mw-content-ltr .mw-content-rtl dd { 475 | margin-right: 1.6em; 476 | margin-left: 0 477 | } 478 | 479 | li.gallerybox { 480 | vertical-align: top; 481 | border: solid 2px white; 482 | display: -moz-inline-box; 483 | display: inline-block 484 | } 485 | 486 | ul.gallery,li.gallerybox { 487 | zoom: 1; 488 | *display: inline 489 | } 490 | 491 | ul.gallery { 492 | margin: 2px; 493 | padding: 2px; 494 | display: block 495 | } 496 | 497 | li.gallerycaption { 498 | font-weight: bold; 499 | text-align: center; 500 | display: block; 501 | word-wrap: break-word 502 | } 503 | 504 | li.gallerybox div.thumb { 505 | text-align: center; 506 | border: 1px solid #ccc; 507 | background-color: #f9f9f9; 508 | margin: 2px 509 | } 510 | 511 | li.gallerybox div.thumb img { 512 | display: block; 513 | margin: 0 auto 514 | } 515 | 516 | div.gallerytext { 517 | overflow: hidden; 518 | font-size: 94%; 519 | padding: 2px 4px; 520 | word-wrap: break-word 521 | } 522 | 523 | h1:lang(as),h1:lang(bn),h1:lang(gu),h1:lang(hi),h1:lang(kn),h1:lang(ml),h1:lang(mr),h1:lang(or),h1:lang(pa),h1:lang(sa),h1:lang(ta),h1:lang(te) 524 | { 525 | line-height: 1.5em !important 526 | } 527 | 528 | h2:lang(as),h3:lang(as),h4:lang(as),h5:lang(as),h6:lang(as),h2:lang(bn),h3:lang(bn),h4:lang(bn),h5:lang(bn),h6:lang(bn),h2:lang(gu),h3:lang(gu),h4:lang(gu),h5:lang(gu),h6:lang(gu),h2:lang(hi),h3:lang(hi),h4:lang(hi),h5:lang(hi),h6:lang(hi),h2:lang(kn),h3:lang(kn),h4:lang(kn),h5:lang(kn),h6:lang(kn),h2:lang(ml),h3:lang(ml),h4:lang(ml),h5:lang(ml),h6:lang(ml),h2:lang(mr),h3:lang(mr),h4:lang(mr),h5:lang(mr),h6:lang(mr),h2:lang(or),h3:lang(or),h4:lang(or),h5:lang(or),h6:lang(or),h2:lang(pa),h3:lang(pa),h4:lang(pa),h5:lang(pa),h6:lang(pa),h2:lang(sa),h3:lang(sa),h4:lang(sa),h5:lang(sa),h6:lang(sa),h2:lang(ta),h3:lang(ta),h4:lang(ta),h5:lang(ta),h6:lang(ta),h2:lang(te),h3:lang(te),h4:lang(te),h5:lang(te),h6:lang(te) 529 | { 530 | line-height: 1.2em 531 | } 532 | 533 | ol:lang(bcc) li,ol:lang(bqi) li,ol:lang(fa) li,ol:lang(glk) li,ol:lang(kk-arab) li,ol:lang(mzn) li 534 | { 535 | list-style-type: -moz-persian; 536 | list-style-type: persian 537 | } 538 | 539 | ol:lang(ckb) li { 540 | list-style-type: -moz-arabic-indic; 541 | list-style-type: arabic-indic 542 | } 543 | 544 | ol:lang(as) li,ol:lang(bn) li { 545 | list-style-type: -moz-bengali; 546 | list-style-type: bengali 547 | } 548 | 549 | ol:lang(or) li { 550 | list-style-type: -moz-oriya; 551 | list-style-type: oriya 552 | } 553 | 554 | #toc ul,.toc ul { 555 | margin: .3em 0 556 | } 557 | 558 | .mw-content-ltr .toc ul,.mw-content-ltr #toc ul,.mw-content-rtl .mw-content-ltr .toc ul,.mw-content-rtl .mw-content-ltr #toc ul 559 | { 560 | text-align: left 561 | } 562 | 563 | .mw-content-rtl .toc ul,.mw-content-rtl #toc ul,.mw-content-ltr .mw-content-rtl .toc ul,.mw-content-ltr .mw-content-rtl #toc ul 564 | { 565 | text-align: right 566 | } 567 | 568 | .mw-content-ltr .toc ul ul,.mw-content-ltr #toc ul ul,.mw-content-rtl .mw-content-ltr .toc ul ul,.mw-content-rtl .mw-content-ltr #toc ul ul 569 | { 570 | margin: 0 0 0 2em 571 | } 572 | 573 | .mw-content-rtl .toc ul ul,.mw-content-rtl #toc ul ul,.mw-content-ltr .mw-content-rtl .toc ul ul,.mw-content-ltr .mw-content-rtl #toc ul ul 574 | { 575 | margin: 0 2em 0 0 576 | } 577 | 578 | #toc #toctitle,.toc #toctitle,#toc .toctitle,.toc .toctitle { 579 | direction: ltr 580 | } 581 | 582 | .mw-help-field-data { 583 | display: block; 584 | background-color: #d6f3ff; 585 | padding: 5px 8px 4px 8px; 586 | border: 1px solid #5dc9f4; 587 | margin-left: 20px 588 | } 589 | 590 | .tipsy { 591 | padding: 5px 5px 10px; 592 | font-size: 12px; 593 | position: absolute; 594 | z-index: 100000; 595 | overflow: visible 596 | } 597 | 598 | .tipsy-inner { 599 | padding: 5px 8px 4px 8px; 600 | background-color: #d6f3ff; 601 | color: black; 602 | border: 1px solid #5dc9f4; 603 | max-width: 300px; 604 | text-align: left 605 | } 606 | 607 | .tipsy-arrow { 608 | position: absolute; 609 | width: 13px; 610 | height: 13px 611 | } 612 | 613 | .tipsy-se .tipsy-arrow { 614 | bottom: -2px; 615 | right: 10px; 616 | background-position: 0% 100% 617 | } 618 | 619 | #mw-clearyourcache,#mw-sitecsspreview,#mw-sitejspreview,#mw-usercsspreview,#mw-userjspreview 620 | { 621 | direction: ltr; 622 | unicode-bidi: embed 623 | } 624 | 625 | .diff-currentversion-title,.diff { 626 | direction: ltr; 627 | unicode-bidi: embed 628 | } 629 | 630 | .diff-contentalign-right td { 631 | direction: rtl; 632 | unicode-bidi: embed 633 | } 634 | 635 | .diff-contentalign-left td { 636 | direction: ltr; 637 | unicode-bidi: embed 638 | } 639 | 640 | .diff-otitle,.diff-ntitle,.diff-lineno { 641 | direction: ltr !important; 642 | unicode-bidi: embed 643 | } 644 | 645 | #mw-revision-info,#mw-revision-info-current,#mw-revision-nav { 646 | direction: ltr; 647 | display: inline 648 | } 649 | 650 | div.tright,div.floatright,table.floatright { 651 | clear: right; 652 | float: right 653 | } 654 | 655 | div.tleft,div.floatleft,table.floatleft { 656 | float: left; 657 | clear: left 658 | } 659 | 660 | div.floatright,table.floatright,div.floatleft,table.floatleft { 661 | position: relative 662 | } 663 | 664 | #mw-credits a { 665 | unicode-bidi: embed 666 | } 667 | 668 | .xdebug-error { 669 | position: absolute; 670 | z-index: 99 671 | } 672 | 673 | a { 674 | text-decoration: none; 675 | color: #0645ad; 676 | background: none 677 | } 678 | 679 | a:visited { 680 | color: #0b0080 681 | } 682 | 683 | a:active { 684 | color: #faa700 685 | } 686 | 687 | a:hover,a:focus { 688 | text-decoration: underline 689 | } 690 | 691 | a.stub { 692 | color: #772233 693 | } 694 | 695 | a.new { 696 | color: #ba0000 697 | } 698 | 699 | a.new:visited { 700 | color: #a55858 701 | } 702 | 703 | .mw-body a.extiw,.mw-body a.extiw:active { 704 | color: #36b 705 | } 706 | 707 | .mw-body a.extiw:visited { 708 | color: #636 709 | } 710 | 711 | .mw-body a.extiw:active { 712 | color: #b63 713 | } 714 | 715 | .mw-body a.external { 716 | color: #36b 717 | } 718 | 719 | .mw-body a.external:visited { 720 | color: #636; 721 | } 722 | 723 | .mw-body a.external:active { 724 | color: #b63 725 | } 726 | 727 | img { 728 | border: none; 729 | vertical-align: middle 730 | } 731 | 732 | hr { 733 | height: 1px; 734 | color: #aaa; 735 | background-color: #aaa; 736 | border: 0; 737 | margin: .2em 0 738 | } 739 | 740 | h1,h2,h3,h4,h5,h6 { 741 | color: black; 742 | background: none; 743 | font-weight: normal; 744 | margin: 0; 745 | overflow: hidden; 746 | padding-top: .5em; 747 | padding-bottom: .17em; 748 | border-bottom: 1px solid #aaa; 749 | width: auto 750 | } 751 | 752 | h1 { 753 | font-size: 188% 754 | } 755 | 756 | h1 .editsection { 757 | font-size: 53% 758 | } 759 | 760 | h2 { 761 | font-size: 150% 762 | } 763 | 764 | h2 .editsection { 765 | font-size: 67% 766 | } 767 | 768 | h3,h4,h5,h6 { 769 | border-bottom: none; 770 | font-weight: bold 771 | } 772 | 773 | h3 { 774 | font-size: 132% 775 | } 776 | 777 | h3 .editsection { 778 | font-size: 76%; 779 | font-weight: normal 780 | } 781 | 782 | h4 { 783 | font-size: 116% 784 | } 785 | 786 | h4 .editsection { 787 | font-size: 86%; 788 | font-weight: normal 789 | } 790 | 791 | h5 { 792 | font-size: 100% 793 | } 794 | 795 | h5 .editsection { 796 | font-weight: normal 797 | } 798 | 799 | h6 { 800 | font-size: 80% 801 | } 802 | 803 | h6 .editsection { 804 | font-size: 125%; 805 | font-weight: normal 806 | } 807 | 808 | h1,h2 { 809 | margin-bottom: .6em 810 | } 811 | 812 | h3,h4,h5 { 813 | margin-bottom: .3em 814 | } 815 | 816 | p { 817 | margin: .4em 0 .5em 0; 818 | line-height: 1.5em 819 | } 820 | 821 | p img { 822 | margin: 0 823 | } 824 | 825 | ul { 826 | line-height: 1.5em; 827 | list-style-type: square; 828 | margin: .3em 0 0 1.6em; 829 | padding: 0 830 | } 831 | 832 | ol { 833 | line-height: 1.5em; 834 | margin: .3em 0 0 3.2em; 835 | padding: 0; 836 | list-style-image: none 837 | } 838 | 839 | li { 840 | margin-bottom: .1em 841 | } 842 | 843 | dt { 844 | font-weight: bold; 845 | margin-bottom: .1em 846 | } 847 | 848 | dl { 849 | margin-top: .2em; 850 | margin-bottom: .5em 851 | } 852 | 853 | dd { 854 | line-height: 1.5em; 855 | margin-left: 1.6em; 856 | margin-bottom: .1em 857 | } 858 | 859 | q { 860 | font-family: Times, "Times New Roman", serif; 861 | font-style: italic 862 | } 863 | 864 | pre,code,tt,kbd,samp { 865 | font-family: monospace, Courier 866 | } 867 | 868 | code { 869 | background-color: #f9f9f9 870 | } 871 | 872 | pre { 873 | padding: 1em; 874 | white-space: pre-wrap; 875 | border: 1px dashed #2f6fab; 876 | color: black; 877 | background-color: #f9f9f9 878 | } 879 | 880 | table { 881 | font-size: 100% 882 | } 883 | 884 | fieldset { 885 | border: 1px solid #2f6fab; 886 | margin: 1em 0 1em 0; 887 | padding: 0 1em 1em; 888 | line-height: 1.5em 889 | } 890 | 891 | fieldset.nested { 892 | margin: 0 0 0.5em 0; 893 | padding: 0 0.5em 0.5em 894 | } 895 | 896 | legend { 897 | padding: .5em; 898 | font-size: 95% 899 | } 900 | 901 | form { 902 | border: none; 903 | margin: 0 904 | } 905 | 906 | textarea { 907 | width: 100%; 908 | padding: .1em 909 | } 910 | 911 | select { 912 | vertical-align: top 913 | } 914 | 915 | .center { 916 | width: 100%; 917 | text-align: center 918 | } 919 | 920 | *.center * { 921 | margin-left: auto; 922 | margin-right: auto 923 | } 924 | 925 | .small { 926 | font-size: 94% 927 | } 928 | 929 | table.small { 930 | font-size: 100% 931 | } 932 | 933 | #toc,.toc,.mw-warning { 934 | border: 1px solid #aaa; 935 | background-color: #f9f9f9; 936 | padding: 5px; 937 | font-size: 95% 938 | } 939 | 940 | #toc h2,.toc h2 { 941 | display: inline; 942 | border: none; 943 | padding: 0; 944 | font-size: 100%; 945 | font-weight: bold 946 | } 947 | 948 | #toc #toctitle,.toc #toctitle,#toc .toctitle,.toc .toctitle { 949 | text-align: center 950 | } 951 | 952 | #toc ul,.toc ul { 953 | list-style-type: none; 954 | list-style-image: none; 955 | margin-left: 0; 956 | padding: 0; 957 | text-align: left 958 | } 959 | 960 | #toc ul ul,.toc ul ul { 961 | margin: 0 0 0 2em 962 | } 963 | 964 | #toc .toctoggle,.toc .toctoggle { 965 | font-size: 94% 966 | } 967 | 968 | .toccolours { 969 | border: 1px solid #aaa; 970 | background-color: #f9f9f9; 971 | padding: 5px; 972 | font-size: 95% 973 | } 974 | 975 | .mw-warning { 976 | margin-left: 50px; 977 | margin-right: 50px; 978 | text-align: center 979 | } 980 | 981 | div.floatright,table.floatright { 982 | margin: 0 0 .5em .5em; 983 | border: 0 984 | } 985 | 986 | div.floatright p { 987 | font-style: italic 988 | } 989 | 990 | div.floatleft,table.floatleft { 991 | margin: 0 .5em .5em 0; 992 | border: 0 993 | } 994 | 995 | div.floatleft p { 996 | font-style: italic 997 | } 998 | 999 | div.thumb { 1000 | margin-bottom: .5em; 1001 | width: auto; 1002 | background-color: transparent 1003 | } 1004 | 1005 | div.thumbinner { 1006 | border: 1px solid #ccc; 1007 | padding: 3px !important; 1008 | background-color: #f9f9f9; 1009 | font-size: 94%; 1010 | text-align: center; 1011 | overflow: hidden 1012 | } 1013 | 1014 | html .thumbimage { 1015 | border: 1px solid #ccc 1016 | } 1017 | 1018 | html .thumbcaption { 1019 | border: none; 1020 | text-align: left; 1021 | line-height: 1.4em; 1022 | padding: 3px !important; 1023 | font-size: 94% 1024 | } 1025 | 1026 | div.magnify { 1027 | float: right; 1028 | border: none !important; 1029 | background: none !important 1030 | } 1031 | 1032 | div.magnify a,div.magnify img { 1033 | display: block; 1034 | border: none !important; 1035 | background: none !important 1036 | } 1037 | 1038 | div.tright { 1039 | margin: .5em 0 1.3em 1.4em 1040 | } 1041 | 1042 | div.tleft { 1043 | margin: .5em 1.4em 1.3em 0 1044 | } 1045 | 1046 | img.thumbborder { 1047 | border: 1px solid #dddddd 1048 | } 1049 | 1050 | #userlogin,#userloginForm { 1051 | border: solid 1px #cccccc; 1052 | padding: 1.2em; 1053 | margin: .5em; 1054 | float: left 1055 | } 1056 | 1057 | .usermessage { 1058 | background-color: #ffce7b; 1059 | border: 1px solid #ffa500; 1060 | color: black; 1061 | font-weight: bold; 1062 | margin: 2em 0 1em; 1063 | padding: .5em 1em; 1064 | vertical-align: middle 1065 | } 1066 | 1067 | #siteNotice { 1068 | position: relative; 1069 | text-align: center; 1070 | margin: 0 1071 | } 1072 | 1073 | #localNotice { 1074 | margin-bottom: 0.9em 1075 | } 1076 | 1077 | .firstHeading,#firstHeading { 1078 | margin-bottom: .1em; 1079 | line-height: 1.2em; 1080 | padding-bottom: 0 1081 | } 1082 | 1083 | #contentSub,#contentSub2 { 1084 | font-size: 84%; 1085 | line-height: 1.2em; 1086 | margin: 0 0 1.4em 1em; 1087 | color: #7d7d7d; 1088 | width: auto 1089 | } 1090 | 1091 | span.subpages { 1092 | display: block 1093 | } 1094 | 1095 | div#column-content { 1096 | width: 100%; 1097 | /*float: right; 1098 | margin: 0 0 .6em -12.2em;*/ 1099 | padding: 0 1100 | } 1101 | 1102 | div#content { /*margin: 2.8em 0 0 12.2em;; 1103 | padding: 0 1em 1em 1em;*/ 1104 | margin: 1em; 1105 | position: relative; 1106 | z-index: 2 1107 | } 1108 | 1109 | div#content { 1110 | background: white; 1111 | color: black; 1112 | /*border: 1px solid #aaa; 1113 | border-right: none;*/ 1114 | line-height: 1.5em 1115 | } 1116 | 1117 | body { 1118 | font: x-small sans-serif; 1119 | /*background: #FFF888;*/ 1120 | color: black; 1121 | margin: 0; 1122 | padding: 0; 1123 | direction: ltr; 1124 | unicode-bidi: embed 1125 | } 1126 | 1127 | div#globalWrapper { 1128 | font-size: 115%; /*default 127%*/ 1129 | width: 100%; 1130 | margin: 0; 1131 | padding: 0 1132 | } 1133 | 1134 | a { 1135 | color: #002bb8 1136 | } 1137 | 1138 | a:visited { 1139 | color: #5a3696 1140 | } 1141 | 1142 | a.new { 1143 | color: #cc2200 1144 | } 1145 | 1146 | input.historysubmit { 1147 | padding: 0 .3em .3em .3em !important; 1148 | font-size: 94%; 1149 | cursor: pointer; 1150 | height: 1.7em !important; 1151 | margin-left: 1.6em 1152 | } 1153 | 1154 | pre { 1155 | line-height: 1.1em 1156 | } 1157 | 1158 | #siteNotice { 1159 | font-size: 95%; 1160 | padding: 0 0.9em 1161 | } 1162 | 1163 | #localNotice { 1164 | margin: 0 1165 | } 1166 | 1167 | #siteNotice p { 1168 | margin: 0; 1169 | padding: 0 1170 | } 1171 | 1172 | table.rimage { 1173 | float: right; 1174 | position: relative; 1175 | margin-left: 1em; 1176 | margin-bottom: 1em; 1177 | text-align: center 1178 | } 1179 | 1180 | .special li { 1181 | line-height: 1.4em; 1182 | margin: 0; 1183 | padding: 0 1184 | } 1185 | 1186 | #bodyContent { 1187 | word-wrap: break-word; 1188 | } 1189 | 1190 | #bodyContent a.extiw,#bodyContent a.extiw:active { 1191 | color: #36b 1192 | } 1193 | 1194 | #bodyContent a.external { 1195 | color: #36b 1196 | } 1197 | 1198 | .pBody { 1199 | font-size: 95%; 1200 | background-color: white; 1201 | color: black; 1202 | border-collapse: collapse; 1203 | border: 1px solid #aaa; 1204 | padding: 0 .8em .3em .5em 1205 | } 1206 | 1207 | #p-logo { 1208 | top: 0; 1209 | left: 0; 1210 | position: absolute; 1211 | z-index: 3; 1212 | height: 155px; 1213 | width: 12em; 1214 | overflow: visible 1215 | } 1216 | 1217 | #p-logo h5 { 1218 | display: none 1219 | } 1220 | 1221 | #p-logo a,#p-logo a:hover { 1222 | display: block; 1223 | height: 155px; 1224 | width: 12.2em; 1225 | background-repeat: no-repeat; 1226 | background-position: 35% 50% !important; 1227 | text-decoration: none 1228 | } 1229 | 1230 | #p-search { 1231 | position: relative; 1232 | z-index: 3 1233 | } 1234 | 1235 | input.searchButton { 1236 | margin-top: 1px; 1237 | font-size: 95% 1238 | } 1239 | 1240 | #searchGoButton { 1241 | padding-left: .5em; 1242 | padding-right: .5em; 1243 | font-weight: bold 1244 | } 1245 | 1246 | #searchInput { 1247 | width: 10.9em; 1248 | margin: 0; 1249 | font-size: 95% 1250 | } 1251 | 1252 | #p-search .pBody { 1253 | padding: .5em .4em .4em .4em; 1254 | text-align: center 1255 | } 1256 | 1257 | #p-search #searchform div div { 1258 | margin-top: .4em; 1259 | font-size: 95% 1260 | } 1261 | 1262 | #t-ispermalink,#t-iscite { 1263 | color: #999 1264 | } 1265 | 1266 | td.htmlform-tip { 1267 | font-size: x-small; 1268 | padding: .2em 2em; 1269 | color: #666 1270 | } 1271 | 1272 | *> ; html #bodyContent,*> ; html #bodyContent pre { 1273 | overflow-x: auto; 1274 | width: 100%; 1275 | padding-bottom: 25px 1276 | } 1277 | 1278 | * html div#column-content { 1279 | display: inline; 1280 | margin-bottom: 0 1281 | } 1282 | 1283 | *> ; html div#column-content { 1284 | float: none 1285 | } 1286 | 1287 | .redirectText { 1288 | font-size: 150%; 1289 | margin: 5px 1290 | } 1291 | 1292 | .printfooter { 1293 | display: none 1294 | } 1295 | 1296 | div.patrollink { 1297 | clear: both 1298 | } 1299 | 1300 | .sharedUploadNotice { 1301 | font-style: italic 1302 | } 1303 | 1304 | span.updatedmarker { 1305 | color: black; 1306 | background-color: #0f0 1307 | } 1308 | 1309 | .toggle { 1310 | margin-left: 2em; 1311 | text-indent: -2em 1312 | } 1313 | 1314 | input#wpSave,input#wpDiff { 1315 | margin-right: 0.33em 1316 | } 1317 | 1318 | #wpSave { 1319 | font-weight: bold 1320 | } 1321 | 1322 | div.noarticletext { 1323 | border: 1px solid #ccc; 1324 | background: #fff; 1325 | padding: .2em 1em; 1326 | color: #000 1327 | } 1328 | 1329 | div#searchTargetContainer { 1330 | left: 10px; 1331 | top: 10px; 1332 | width: 90%; 1333 | background: white 1334 | } 1335 | 1336 | div#searchTarget { 1337 | padding: 3px; 1338 | margin: 5px; 1339 | background: #F0F0F0; 1340 | border: solid 1px blue 1341 | } 1342 | 1343 | div#searchTarget ul li { 1344 | list-style: none 1345 | } 1346 | 1347 | div#searchTarget ul li:before { 1348 | color: orange; 1349 | content: "\00BB \0020" 1350 | } 1351 | 1352 | div#searchTargetHide { 1353 | float: right; 1354 | border: solid 1px black; 1355 | background: #DCDCDC; 1356 | padding: 2px 1357 | } 1358 | 1359 | #powersearch p { 1360 | margin-top: 0px 1361 | } 1362 | 1363 | div.multipageimagenavbox { 1364 | border: solid 1px silver; 1365 | padding: 4px; 1366 | margin: 1em; 1367 | background: #f0f0f0 1368 | } 1369 | 1370 | div.multipageimagenavbox div.thumb { 1371 | border: none; 1372 | margin-left: 2em; 1373 | margin-right: 2em 1374 | } 1375 | 1376 | div.multipageimagenavbox hr { 1377 | margin: 6px 1378 | } 1379 | 1380 | table.multipageimage td { 1381 | text-align: center 1382 | } 1383 | 1384 | .templatesUsed { 1385 | margin-top: 1.5em 1386 | } 1387 | 1388 | .mw-summary-preview { 1389 | margin: 0.1em 0 1390 | } 1391 | 1392 | div.mw-lag-warn-normal,div.mw-lag-warn-high { 1393 | padding: 3px; 1394 | text-align: center; 1395 | margin: 3px auto 1396 | } 1397 | 1398 | div.mw-lag-warn-normal { 1399 | border: 1px solid #FFCC66; 1400 | background-color: #FFFFCC 1401 | } 1402 | 1403 | div.mw-lag-warn-high { 1404 | font-weight: bold; 1405 | border: 2px solid #FF0033; 1406 | background-color: #FFCCCC 1407 | } 1408 | 1409 | .MediaTransformError { 1410 | background-color: #ccc; 1411 | padding: 0.1em 1412 | } 1413 | 1414 | .MediaTransformError td { 1415 | text-align: center; 1416 | vertical-align: middle; 1417 | font-size: 90% 1418 | } 1419 | 1420 | .no-text-transform { 1421 | text-transform: none 1422 | } 1423 | 1424 | .tipsy { 1425 | font-size: 127% 1426 | } 1427 | 1428 | body { /*background: #f6f9fc*/ 1429 | background: #fff 1430 | } 1431 | 1432 | body,#content,table { 1433 | color: #222 1434 | } 1435 | 1436 | h1,h2,h3,h4,h5 { 1437 | color: #222 1438 | } 1439 | 1440 | h1 { 1441 | font-weight: bold 1442 | } 1443 | 1444 | pre,code,tt { 1445 | background-color: #ebf1f5; 1446 | color: #222; 1447 | font-family: monospace 1448 | } 1449 | 1450 | pre { 1451 | border: 1px solid #bcd; 1452 | overflow: auto 1453 | } 1454 | 1455 | code,tt { 1456 | padding: 0.3em 1457 | } 1458 | 1459 | a { 1460 | text-decoration: none; 1461 | outline: none 1462 | } 1463 | 1464 | a:link,#bodyContent a.external { 1465 | color: #07b 1466 | } 1467 | 1468 | #bodyContent> ; div.mw-content-ltr a,#bodyContent> ; div.mw-content-rtl a,#wikiPreview> 1469 | ; div.mw-content-ltr a,#wikiPreview> ; div.mw-content-rtl a { 1470 | font-weight: bold 1471 | } 1472 | 1473 | #bodyContent #toc a,#bodyContent .special li> ; a,#bodyContent .special li span a,#bodyContent #pagehistory a 1474 | { 1475 | font-weight: normal 1476 | } 1477 | 1478 | a:visited,#bodyContent a:visited.external { 1479 | color: #666 1480 | } 1481 | 1482 | a:focus { 1483 | color: #e90 !important 1484 | } 1485 | 1486 | a:hover,#bodyContent #toc a:hover,#bodyContent a:hover.external { 1487 | text-decoration: underline; 1488 | background-color: transparent; 1489 | color: #999 1490 | } 1491 | 1492 | a:active { 1493 | color: #e90 !important 1494 | } 1495 | 1496 | a.new { 1497 | color: #b00 !important 1498 | } 1499 | 1500 | /*#content { 1501 | top: .8em 1502 | } 1503 | 1504 | #content { 1505 | top: 10px 1506 | }*/ 1507 | div#globalWrapper { 1508 | width: 100% 1509 | } 1510 | 1511 | #toc,.toc,.mw-warning { 1512 | background-color: #f9faff; 1513 | border: 1px solid #d7dfe3 1514 | } 1515 | 1516 | .pBody { 1517 | border: 1px solid #ddd 1518 | } 1519 | 1520 | div#content { /*border: 1px solid #ccc*/ 1521 | 1522 | } 1523 | 1524 | #p-logo { 1525 | display: none !important 1526 | } 1527 | 1528 | #bodyContent a.external[href^="https://"],.link-https { 1529 | background: none; 1530 | padding: 0 1531 | } 1532 | 1533 | #bodyContent table { 1534 | border-collapse: collapse; 1535 | padding: 2px 1536 | } 1537 | 1538 | #bodyContent td { 1539 | padding: 2px 1540 | } 1541 | 1542 | ul,.portlet ul { 1543 | list-style-image: none 1544 | } 1545 | 1546 | /***hide things***/ 1547 | #jump-to-nav,#siteSub,#archnavbar,#footer,#column-one,#catlinks { 1548 | display: none !important; 1549 | } 1550 | 1551 | h1.firstHeading { /*hide title*/ 1552 | display: none; 1553 | } 1554 | 1555 | div.printfooter,div.mw-search-formheader,div.mw-search-result-data { 1556 | display: none; 1557 | } 1558 | 1559 | input { /*hide search*/ 1560 | display: none; 1561 | } 1562 | 1563 | /***format page***/ 1564 | #content { /* width: 99% !important;*/ 1565 | background-color: #F6F9FC; 1566 | } 1567 | 1568 | li.toclevel-1 { 1569 | padding: 5px; 1570 | } 1571 | 1572 | li.toclevel-2 { 1573 | padding: 5px; 1574 | } 1575 | 1576 | li.toclevel-3 { 1577 | display: none; 1578 | } 1579 | 1580 | div.mw-search-result-heading { 1581 | padding-top: 10px; 1582 | } 1583 | /*table th{ 1584 | border: 1px solid #333 1585 | } 1586 | */ 1587 | /*this is needed for "Summary" and "Related" */ 1588 | div#mw-content-text.mw-content-ltr table { 1589 | /*background-color: #F97;*/ 1590 | width: 100% !important; 1591 | float: none !important; 1592 | clear: none !important; 1593 | position: relative !important; 1594 | margin: 0em; 1595 | left: -5px; 1596 | /*display: inline-block !important;*/ 1597 | /*border: 1px solid #BCD*/ 1598 | } 1599 | 1600 | 1601 | table#disputed.notice.noprint.toc { 1602 | /*background-color: #F17;*/ 1603 | /*position: absolute; 1604 | top: 0; 1605 | left: 0; 1606 | width: 100%;*/ 1607 | /*display: block !important;*/ 1608 | } -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/github/takahirom/webview_in_coodinator_layout/NestedWebView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 takahirom 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.takahirom.webview_in_coodinator_layout; 18 | 19 | import android.content.Context; 20 | 21 | import androidx.core.view.MotionEventCompat; 22 | import androidx.core.view.NestedScrollingChild; 23 | import androidx.core.view.NestedScrollingChildHelper; 24 | import androidx.core.view.ViewCompat; 25 | 26 | import android.util.AttributeSet; 27 | import android.view.MotionEvent; 28 | import android.webkit.WebView; 29 | 30 | public class NestedWebView extends WebView implements NestedScrollingChild { 31 | private final int[] mScrollOffset = new int[2]; 32 | private final int[] mScrollConsumed = new int[2]; 33 | private int mLastY; 34 | private int mNestedOffsetY; 35 | private NestedScrollingChildHelper mChildHelper; 36 | 37 | public NestedWebView(Context context) { 38 | this(context, null); 39 | } 40 | 41 | public NestedWebView(Context context, AttributeSet attrs) { 42 | this(context, attrs, android.R.attr.webViewStyle); 43 | } 44 | 45 | public NestedWebView(Context context, AttributeSet attrs, int defStyleAttr) { 46 | super(context, attrs, defStyleAttr); 47 | mChildHelper = new NestedScrollingChildHelper(this); 48 | setNestedScrollingEnabled(true); 49 | } 50 | 51 | @Override 52 | public boolean onTouchEvent(MotionEvent ev) { 53 | boolean returnValue = false; 54 | 55 | MotionEvent event = MotionEvent.obtain(ev); 56 | final int action = MotionEventCompat.getActionMasked(event); 57 | if (action == MotionEvent.ACTION_DOWN) { 58 | mNestedOffsetY = 0; 59 | } 60 | int eventY = (int) event.getY(); 61 | event.offsetLocation(0, mNestedOffsetY); 62 | switch (action) { 63 | case MotionEvent.ACTION_MOVE: 64 | int deltaY = mLastY - eventY; 65 | // NestedPreScroll 66 | if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { 67 | deltaY -= mScrollConsumed[1]; 68 | mLastY = eventY - mScrollOffset[1]; 69 | event.offsetLocation(0, -mScrollOffset[1]); 70 | mNestedOffsetY += mScrollOffset[1]; 71 | } 72 | returnValue = super.onTouchEvent(event); 73 | 74 | // NestedScroll 75 | if (dispatchNestedScroll(0, mScrollOffset[1], 0, deltaY, mScrollOffset)) { 76 | event.offsetLocation(0, mScrollOffset[1]); 77 | mNestedOffsetY += mScrollOffset[1]; 78 | mLastY -= mScrollOffset[1]; 79 | } 80 | break; 81 | case MotionEvent.ACTION_DOWN: 82 | returnValue = super.onTouchEvent(event); 83 | mLastY = eventY; 84 | // start NestedScroll 85 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 86 | break; 87 | case MotionEvent.ACTION_UP: 88 | case MotionEvent.ACTION_CANCEL: 89 | returnValue = super.onTouchEvent(event); 90 | // end NestedScroll 91 | stopNestedScroll(); 92 | break; 93 | } 94 | return returnValue; 95 | } 96 | 97 | @Override 98 | public boolean isNestedScrollingEnabled() { 99 | return mChildHelper.isNestedScrollingEnabled(); 100 | } 101 | 102 | // Nested Scroll implements 103 | @Override 104 | public void setNestedScrollingEnabled(boolean enabled) { 105 | mChildHelper.setNestedScrollingEnabled(enabled); 106 | } 107 | 108 | @Override 109 | public boolean startNestedScroll(int axes) { 110 | return mChildHelper.startNestedScroll(axes); 111 | } 112 | 113 | @Override 114 | public void stopNestedScroll() { 115 | mChildHelper.stopNestedScroll(); 116 | } 117 | 118 | @Override 119 | public boolean hasNestedScrollingParent() { 120 | return mChildHelper.hasNestedScrollingParent(); 121 | } 122 | 123 | @Override 124 | public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, 125 | int[] offsetInWindow) { 126 | return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); 127 | } 128 | 129 | @Override 130 | public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { 131 | return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); 132 | } 133 | 134 | @Override 135 | public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { 136 | return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); 137 | } 138 | 139 | @Override 140 | public boolean dispatchNestedPreFling(float velocityX, float velocityY) { 141 | return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); 142 | } 143 | 144 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/ArchWikiViewerApp.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer 2 | 3 | import android.app.Application 4 | import timber.log.Timber 5 | import timber.log.Timber.DebugTree 6 | 7 | 8 | class ArchwikiViewerApp : Application() { 9 | override fun onCreate() { 10 | super.onCreate() 11 | Timber.plant(DebugTree()) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer 2 | 3 | 4 | //format types 5 | const val TEXT_HTML_MIME = "text/html" 6 | const val TEXT_PLAIN_MIME = "text/plain" 7 | const val UTF_8 = "UTF-8" 8 | 9 | //arch wiki urls 10 | const val ARCHWIKI_BASE = "https://wiki.archlinux.org" 11 | const val ARCHWIKI_MAIN = "${ARCHWIKI_BASE}/index.php/Main_page" 12 | const val ARCHWIKI_SEARCH_URL = "${ARCHWIKI_BASE}/index.php?&search=%s" 13 | 14 | //local file paths 15 | const val ASSETS_FOLDER = "file:///android_asset" 16 | const val LOCAL_CSS = "${ASSETS_FOLDER}/style.css" 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer 2 | 3 | import android.app.SearchManager 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.view.Menu 8 | import android.view.MenuItem 9 | import androidx.appcompat.app.AppCompatActivity 10 | import androidx.appcompat.widget.SearchView 11 | import com.jtmcn.archwiki.viewer.data.SearchResult 12 | import com.jtmcn.archwiki.viewer.data.getSearchQuery 13 | import com.jtmcn.archwiki.viewer.tasks.Fetch 14 | import com.jtmcn.archwiki.viewer.utils.getTextZoom 15 | import kotlinx.android.synthetic.main.activity_main.* 16 | import kotlinx.android.synthetic.main.toolbar.* 17 | import timber.log.Timber 18 | import java.util.* 19 | 20 | class MainActivity : AppCompatActivity() { 21 | private lateinit var searchView: SearchView 22 | private lateinit var searchMenuItem: MenuItem 23 | private var currentSuggestions: List? = null 24 | 25 | override fun onCreate(savedInstanceState: Bundle?) { 26 | super.onCreate(savedInstanceState) 27 | setContentView(R.layout.activity_main) 28 | 29 | setSupportActionBar(toolbar) 30 | 31 | wikiViewer.buildView(progressBar, supportActionBar) 32 | 33 | handleIntent(intent) 34 | } 35 | 36 | override fun onResume() { 37 | super.onResume() 38 | updateWebSettings() 39 | } 40 | 41 | override fun onNewIntent(intent: Intent) { 42 | super.onNewIntent(intent) 43 | handleIntent(intent) 44 | } 45 | 46 | private fun handleIntent(intent: Intent?) { 47 | if (intent == null) { 48 | return 49 | } 50 | 51 | if (Intent.ACTION_SEARCH == intent.action) { 52 | val query = intent.getStringExtra(SearchManager.QUERY) 53 | wikiViewer.passSearch(query!!) 54 | hideSearchView() 55 | } else if (Intent.ACTION_VIEW == intent.action) { 56 | val url = intent.dataString 57 | wikiViewer.wikiClient.shouldOverrideUrlLoading(wikiViewer, url!!) 58 | } 59 | } 60 | 61 | /** 62 | * Update the font size used in the webview. 63 | */ 64 | private fun updateWebSettings() { 65 | wikiViewer.settings.textZoom = getTextZoom(this) 66 | } 67 | 68 | override fun onPrepareOptionsMenu(menu: Menu): Boolean { 69 | val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager 70 | searchMenuItem = menu.findItem(R.id.menu_search) 71 | searchView = searchMenuItem.actionView as SearchView 72 | searchView.setOnQueryTextFocusChangeListener { _, hasFocus -> 73 | if (!hasFocus) { 74 | hideSearchView() 75 | } 76 | } 77 | searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName)) 78 | searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { 79 | override fun onQueryTextSubmit(query: String): Boolean { 80 | wikiViewer.passSearch(query) 81 | return false 82 | } 83 | 84 | override fun onQueryTextChange(newText: String): Boolean { 85 | if (newText.isEmpty()) { 86 | setCursorAdapter(ArrayList()) 87 | return true 88 | } else { 89 | val searchUrl = getSearchQuery(newText) 90 | Fetch.search({ 91 | currentSuggestions = it 92 | setCursorAdapter(currentSuggestions) 93 | }, searchUrl) 94 | return true 95 | } 96 | } 97 | }) 98 | 99 | searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener { 100 | override fun onSuggestionSelect(position: Int): Boolean { 101 | return false 102 | } 103 | 104 | override fun onSuggestionClick(position: Int): Boolean { 105 | val (pageName, pageURL) = currentSuggestions!![position] 106 | Timber.d("Opening '$pageName' from search suggestion.") 107 | wikiViewer.wikiClient.shouldOverrideUrlLoading(wikiViewer, pageURL) 108 | hideSearchView() 109 | return true 110 | } 111 | }) 112 | return true 113 | } 114 | 115 | private fun hideSearchView() { 116 | searchMenuItem.collapseActionView() 117 | wikiViewer.requestFocus() //pass control back to the wikiview 118 | } 119 | 120 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 121 | val inflater = menuInflater 122 | inflater.inflate(R.menu.menu, menu) 123 | return true 124 | } 125 | 126 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 127 | when (item.itemId) { 128 | R.id.menu_share -> { 129 | val wikiPage = wikiViewer.currentWebPage 130 | if (wikiPage != null) { 131 | val sharingIntent = Intent() 132 | sharingIntent.type = TEXT_PLAIN_MIME 133 | sharingIntent.action = Intent.ACTION_SEND 134 | sharingIntent.putExtra(Intent.EXTRA_TITLE, wikiPage.pageTitle) 135 | sharingIntent.putExtra(Intent.EXTRA_TEXT, wikiPage.pageUrl) 136 | startActivity(Intent.createChooser(sharingIntent, null)) 137 | } 138 | } 139 | R.id.refresh -> wikiViewer.onRefresh() 140 | R.id.menu_settings -> startActivity(Intent(this, PreferencesActivity::class.java)) 141 | R.id.exit -> finish() 142 | } 143 | return super.onOptionsItemSelected(item) 144 | } 145 | 146 | private fun setCursorAdapter(currentSuggestions: List?) { 147 | searchView.suggestionsAdapter = SearchResultsAdapter.getCursorAdapter(this, currentSuggestions!!) 148 | } 149 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/PreferencesActivity.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.preference.PreferenceFragmentCompat 6 | import com.jtmcn.archwiki.viewer.utils.getTextZoom 7 | import kotlinx.android.synthetic.main.toolbar.* 8 | 9 | class PreferencesActivity : AppCompatActivity() { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.activity_preferences) 14 | 15 | setSupportActionBar(toolbar) 16 | 17 | supportFragmentManager 18 | .beginTransaction() 19 | .replace(R.id.settings, SettingsFragment()) 20 | .commit() 21 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 22 | 23 | getTextZoom(this) 24 | } 25 | 26 | class SettingsFragment : PreferenceFragmentCompat() { 27 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 28 | setPreferencesFromResource(R.xml.preferences, rootKey) 29 | } 30 | } 31 | } 32 | 33 | object Prefs { 34 | @Deprecated(message = "Should use textZoom", replaceWith = ReplaceWith("KEY_TEXT_ZOOM")) 35 | const val KEY_TEXT_SIZE = "textSize" 36 | const val KEY_TEXT_ZOOM = "textZoom" 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/SearchResultsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer 2 | 3 | import android.app.SearchManager 4 | import android.content.Context 5 | import android.database.MatrixCursor 6 | import android.provider.BaseColumns 7 | import androidx.cursoradapter.widget.CursorAdapter 8 | import androidx.cursoradapter.widget.SimpleCursorAdapter 9 | 10 | import com.jtmcn.archwiki.viewer.data.SearchResult 11 | 12 | /** 13 | * Helper for creating a [SimpleCursorAdapter] which will 14 | * list the search results for a [android.widget.SearchView] 15 | */ 16 | object SearchResultsAdapter { 17 | private val columnNames = arrayOf(BaseColumns._ID, SearchManager.SUGGEST_COLUMN_TEXT_1) 18 | private val from = arrayOf(SearchManager.SUGGEST_COLUMN_TEXT_1) 19 | private val to = intArrayOf(R.id.url) 20 | 21 | /** 22 | * Creates a cursor adapter given a [<]. 23 | * https://stackoverflow.com/questions/11628172/converting-an-arrayadapter-to-cursoradapter-for-use-in-a-searchview/11628527#11628527 24 | * 25 | * @param results the results to be placed in the adapter. 26 | * @return the adapter. 27 | */ 28 | fun getCursorAdapter(context: Context, results: List): CursorAdapter { 29 | var id = 0 30 | val cursor = MatrixCursor(columnNames) 31 | for ((pageName) in results) { 32 | val temp = arrayOfNulls(2) 33 | temp[0] = id.toString() // "_id" 34 | temp[1] = pageName // "title" 35 | 36 | cursor.addRow(temp) 37 | id++ 38 | } 39 | 40 | return SimpleCursorAdapter( 41 | context, 42 | R.layout.search_suggestions_list_item, 43 | cursor, 44 | from, 45 | to, 46 | CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/WikiClient.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer 2 | 3 | import android.os.Handler 4 | import android.view.View 5 | import android.webkit.WebView 6 | import android.webkit.WebViewClient 7 | import android.widget.ProgressBar 8 | import androidx.appcompat.app.ActionBar 9 | import com.jtmcn.archwiki.viewer.data.WikiPage 10 | import com.jtmcn.archwiki.viewer.tasks.Fetch 11 | import com.jtmcn.archwiki.viewer.utils.openLink 12 | import timber.log.Timber 13 | import java.util.* 14 | 15 | 16 | class WikiClient(private val progressBar: ProgressBar, private val actionBar: ActionBar?, private val webView: WebView) : WebViewClient() { 17 | private val webPageStack = Stack() 18 | private val loadedUrls = HashSet() // this is used to see if we should restore the scroll position 19 | private var lastLoadedUrl: String? = null //https://stackoverflow.com/questions/11601134/android-webview-function-onpagefinished-is-called-twice 20 | 21 | /** 22 | * Get the number of pages that are in the history. 23 | * 24 | * @return number of pages on the stack. 25 | */ 26 | val historyStackSize: Int 27 | get() = webPageStack.size 28 | 29 | /** 30 | * Returns null or the current page. 31 | * 32 | * @return The current page 33 | */ 34 | val currentWebPage: WikiPage? 35 | get() = if (webPageStack.size == 0) null else webPageStack.peek() 36 | 37 | /* 38 | * Manage page history 39 | */ 40 | private fun addHistory(wikiPage: WikiPage) { 41 | if (webPageStack.size > 0) { 42 | Timber.d("Saving ${currentWebPage?.pageTitle} at ${webView.scrollY}") 43 | currentWebPage!!.scrollPosition = webView.scrollY 44 | } 45 | webPageStack.push(wikiPage) 46 | Timber.i("Adding page ${wikiPage.pageTitle}. Stack size= ${webPageStack.size}") 47 | } 48 | 49 | /** 50 | * Loads the html from a [WikiPage] into the webview. 51 | * 52 | * @param wikiPage the page to be loaded. 53 | */ 54 | fun loadWikiHtml(wikiPage: WikiPage) { 55 | webView.loadDataWithBaseURL( 56 | wikiPage.pageUrl, 57 | wikiPage.htmlString, 58 | TEXT_HTML_MIME, 59 | UTF_8, 60 | null 61 | ) 62 | 63 | setSubtitle(wikiPage.pageTitle) 64 | } 65 | 66 | /** 67 | * Intercept url when clicked. If it's part of the wiki load it here. 68 | * If not, open the device's default browser. 69 | * 70 | * @param view webview being loaded into 71 | * @param url url being loaded 72 | * @return true if should override url loading 73 | */ 74 | override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { 75 | // deprecated until min api 21 is used 76 | if (url.startsWith(ARCHWIKI_BASE)) { 77 | webView.stopLoading() 78 | Fetch.page({ 79 | addHistory(it) 80 | loadWikiHtml(currentWebPage!!) 81 | }, url) 82 | showProgress() 83 | 84 | return false 85 | } else { 86 | openLink(url, view.context) 87 | return true 88 | } 89 | } 90 | 91 | override fun onPageFinished(view: WebView, url: String) { 92 | super.onPageFinished(view, url) 93 | val currentWebPage = currentWebPage 94 | Timber.d("Calling onPageFinished(view, ${currentWebPage?.pageTitle})") 95 | // make sure we're loading the current page and that 96 | // this page's url doesn't have an anchor (only on first page load) 97 | if (url == currentWebPage?.pageUrl && url != lastLoadedUrl) { 98 | if (!isFirstLoad(currentWebPage)) { 99 | Handler().postDelayed({ 100 | val scrollY = currentWebPage.scrollPosition 101 | Timber.d("Restoring ${currentWebPage.pageTitle} at $scrollY") 102 | webView.scrollY = scrollY 103 | }, 25) 104 | } 105 | 106 | lastLoadedUrl = url 107 | hideProgress() 108 | } 109 | } 110 | 111 | private fun isFirstLoad(currentWebPage: WikiPage): Boolean { 112 | return if (loadedUrls.contains(currentWebPage.pageUrl)) { 113 | false 114 | } else { 115 | loadedUrls.add(currentWebPage.pageUrl) 116 | true 117 | } 118 | } 119 | 120 | private fun showProgress() { 121 | progressBar.visibility = View.VISIBLE 122 | } 123 | 124 | private fun hideProgress() { 125 | progressBar.visibility = View.GONE 126 | } 127 | 128 | private fun setSubtitle(title: String?) { 129 | actionBar?.subtitle = title 130 | } 131 | 132 | /** 133 | * Go back to the last loaded page. 134 | */ 135 | fun goBackHistory() { 136 | val (pageUrl, pageTitle) = webPageStack.pop() 137 | loadedUrls.remove(pageUrl) 138 | Timber.i("Removing $pageTitle from stack") 139 | val newPage = webPageStack.peek() 140 | loadWikiHtml(newPage) 141 | } 142 | 143 | fun refreshPage() { 144 | lastLoadedUrl = null // set to null if page should restore position, otherwise start at top of page 145 | val currentWebPage = currentWebPage 146 | if (currentWebPage != null) { 147 | val scrollPosition = currentWebPage.scrollPosition 148 | 149 | val url = currentWebPage.pageUrl 150 | showProgress() 151 | Fetch.page({ wikiPage -> 152 | webPageStack.pop() 153 | webPageStack.push(wikiPage) 154 | wikiPage.scrollPosition = scrollPosition 155 | loadWikiHtml(wikiPage) 156 | }, url) 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/WikiView.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.util.AttributeSet 6 | import android.view.KeyEvent 7 | import android.webkit.WebSettings 8 | import android.widget.ProgressBar 9 | import androidx.appcompat.app.ActionBar 10 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout 11 | import com.github.takahirom.webview_in_coodinator_layout.NestedWebView 12 | import com.jtmcn.archwiki.viewer.data.WikiPage 13 | import timber.log.Timber 14 | 15 | class WikiView(context: Context, attrs: AttributeSet) : NestedWebView(context, attrs), SwipeRefreshLayout.OnRefreshListener { 16 | lateinit var wikiClient: WikiClient 17 | 18 | /** 19 | * Returns the current [WikiPage] being shown or null. 20 | * 21 | * @return current wiki page being shown. 22 | */ 23 | val currentWebPage: WikiPage? 24 | get() = wikiClient.currentWebPage 25 | 26 | init { 27 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !isInEditMode) { 28 | // This allows the webview to inject the css (otherwise it blocks it for security reasons) 29 | settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW 30 | } 31 | } 32 | 33 | /** 34 | * Initializes the wiki client and loads the main page. 35 | */ 36 | fun buildView(progressBar: ProgressBar, actionBar: ActionBar?) { 37 | wikiClient = WikiClient(progressBar, actionBar, this) 38 | webViewClient = wikiClient 39 | wikiClient.shouldOverrideUrlLoading(this, ARCHWIKI_MAIN) 40 | } 41 | 42 | override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { 43 | if (keyCode == KeyEvent.KEYCODE_BACK && wikiClient.historyStackSize > 1) { 44 | Timber.i("Loading previous page.") 45 | Timber.d("Position on page currently at $scrollY") 46 | wikiClient.goBackHistory() 47 | return true 48 | } else { 49 | Timber.d("Passing up button press.") 50 | return super.onKeyDown(keyCode, event) 51 | } 52 | } 53 | 54 | /** 55 | * Performs a search against the wiki. 56 | * 57 | * @param query the text to search for. 58 | */ 59 | fun passSearch(query: String) { 60 | Timber.d("Searching for $query") 61 | val searchUrl = String.format(ARCHWIKI_SEARCH_URL, query) 62 | wikiClient.shouldOverrideUrlLoading(this, searchUrl) 63 | } 64 | 65 | override fun onRefresh() { 66 | wikiClient.refreshPage() 67 | stopLoading() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/data/SearchResult.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer.data 2 | 3 | /** 4 | * A page on the wiki which only knows the name and url. 5 | */ 6 | data class SearchResult(val pageName: String, val pageURL: String) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/data/SearchResultsBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer.data 2 | 3 | import com.google.gson.JsonParser 4 | import com.jtmcn.archwiki.viewer.ARCHWIKI_BASE 5 | 6 | /** 7 | * Builds a string url to fetch search results. 8 | * 9 | * @param query the text to search for. 10 | * @param limit the maximum number of results to retrieve. 11 | * @return a url to fetch. 12 | */ 13 | fun getSearchQuery(query: String, limit: Int = 10): String { 14 | return "${ARCHWIKI_BASE}/api.php?" + 15 | "action=opensearch&format=json&formatversion=2&namespace=0&suggest=true" + 16 | "&search=$query" + 17 | "&limit=$limit" 18 | } 19 | 20 | /** 21 | * Builds a [List] from the result of fetching with [getSearchQuery]. 22 | * 23 | * @param jsonResult the string returned from the query. 24 | * @return a parsed list of the results. 25 | */ 26 | fun parseSearchResults(jsonResult: String): List { 27 | val jsonRoot = JsonParser().parse(jsonResult) 28 | if (!jsonRoot.isJsonArray || jsonRoot.asJsonArray.size() != 4) return listOf() 29 | 30 | val jsonArray = jsonRoot.asJsonArray 31 | 32 | val listOfPageTitles = jsonArray.get(1).asJsonArray.mapNotNull { it.asString } 33 | val listOfPageUrls = jsonArray.get(3).asJsonArray.mapNotNull { it.asString } 34 | 35 | return listOfPageTitles.zip(listOfPageUrls).map { SearchResult(it.first, it.second) } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/data/WikiPage.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer.data 2 | 3 | /** 4 | * Wrapper for a downloaded wiki page which holds the title and html. 5 | */ 6 | data class WikiPage(val pageUrl: String, val pageTitle: String?, val htmlString: String) { 7 | var scrollPosition = 0 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/data/WikiPageBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer.data 2 | 3 | import com.jtmcn.archwiki.viewer.LOCAL_CSS 4 | 5 | /** 6 | * Helps with creating a [WikiPage] by extracting content from the 7 | * html fetched from the ArchWiki. 8 | */ 9 | 10 | //NOTE: spaces are allowed in ""/etc, but parsing this way should be fine 11 | const val HTML_HEAD_OPEN = "" 12 | const val HTML_HEAD_CLOSE = "" 13 | const val HTML_TITLE_OPEN = "" 14 | const val HTML_TITLE_CLOSE = "" 15 | private const val HEAD_TO_INJECT = "" + 16 | "" 17 | private const val DEFAULT_TITLE = " - ArchWiki" 18 | 19 | /** 20 | * Builds a page containing the title, url, and injects local css. 21 | * 22 | * @param url url to download. 23 | * @param html [StringBuilder] containing the html of the wikipage 24 | * @return [WikiPage] containing downloaded page. 25 | */ 26 | fun buildPage(url: String, html: StringBuilder): WikiPage { 27 | val pageTitle = getPageTitle(html) 28 | injectLocalCSS(html, LOCAL_CSS) 29 | return WikiPage(url, pageTitle, html.toString()) 30 | } 31 | 32 | /** 33 | * Finds the name of the page within the title block of the html. 34 | * The returned string removes the " - ArchWiki" if found. 35 | * 36 | * @param htmlString The html of the page as a string. 37 | * @return the extracted title from the page. 38 | */ 39 | fun getPageTitle(htmlString: StringBuilder): String? { 40 | val titleStart = htmlString.indexOf(HTML_TITLE_OPEN) + HTML_TITLE_OPEN.length 41 | val titleEnd = htmlString.indexOf(HTML_TITLE_CLOSE, titleStart) 42 | if (titleStart in 1..titleEnd) { // if there is an html title block 43 | val title = htmlString.substring(titleStart, titleEnd) 44 | return title.replace(DEFAULT_TITLE, "") // drop DEFAULT_TITLE from page title 45 | } 46 | return null 47 | } 48 | 49 | /** 50 | * Removes the contents within the head block of the html 51 | * and replaces it with the a reference to a local css file. 52 | * 53 | * @param htmlString The html of the page as a string. 54 | * @param localCSSFilePath The path of the css file to inject. 55 | * @return true if the block was successfully replaced. 56 | */ 57 | fun injectLocalCSS(htmlString: StringBuilder, localCSSFilePath: String): Boolean { 58 | val headStart = htmlString.indexOf(HTML_HEAD_OPEN) + HTML_HEAD_OPEN.length 59 | val headEnd = htmlString.indexOf(HTML_HEAD_CLOSE, headStart) 60 | 61 | if (headStart in 1..headEnd) { 62 | val injectedHeadHtml = String.format(HEAD_TO_INJECT, localCSSFilePath) 63 | htmlString.replace(headStart, headEnd, injectedHeadHtml) 64 | return true 65 | } 66 | 67 | return false 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/tasks/Fetch.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer.tasks 2 | 3 | import android.os.AsyncTask 4 | import com.jtmcn.archwiki.viewer.data.SearchResult 5 | import com.jtmcn.archwiki.viewer.data.WikiPage 6 | import com.jtmcn.archwiki.viewer.data.buildPage 7 | import com.jtmcn.archwiki.viewer.data.parseSearchResults 8 | 9 | /** 10 | * Wrapper for [FetchUrl] which gives an easy to use interface 11 | * for fetching [SearchResult] and [WikiPage]. 12 | */ 13 | object Fetch { 14 | private val SEARCH_RESULTS_MAPPER = { _: String, html: StringBuilder -> parseSearchResults(html.toString()) } 15 | 16 | private val WIKI_PAGE_MAPPER = { url: String, html: StringBuilder -> buildPage(url, html) } 17 | 18 | /** 19 | * Fetches a List from the url. 20 | * 21 | * @param onFinish The listener called when search results are ready. 22 | * @param url The url to fetch the search results from. 23 | * @return the async task fetching the data. 24 | */ 25 | fun search(onFinish: (List) -> Unit, url: String): AsyncTask> { 26 | return FetchUrl(onFinish, SEARCH_RESULTS_MAPPER).execute(url) 27 | } 28 | 29 | /** 30 | * Fetches a [WikiPage] from the url. 31 | * 32 | * @param onFinish The listener called when the page is ready. 33 | * @param url The url to fetch the page from. 34 | * @return the async task fetching the data. 35 | */ 36 | fun page(onFinish: (WikiPage) -> Unit, url: String): AsyncTask { 37 | return FetchUrl(onFinish, WIKI_PAGE_MAPPER).execute(url) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/tasks/FetchUrl.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer.tasks 2 | 3 | import android.os.AsyncTask 4 | import com.jtmcn.archwiki.viewer.utils.fetchURL 5 | import timber.log.Timber 6 | import java.io.IOException 7 | 8 | /** 9 | * Fetches a url, [mapper] maps it to a [Result], and returns it. 10 | * */ 11 | class FetchUrl( 12 | private val onFinish: (Result) -> Unit, 13 | private val mapper: (url: String, html: StringBuilder) -> Result 14 | ) : AsyncTask() { 15 | 16 | override fun doInBackground(vararg params: String): Result? { 17 | if (params.isNotEmpty()) { 18 | val url = params[0] 19 | val toAdd = getItem(url) 20 | return mapper(url, toAdd) 21 | } 22 | return null 23 | } 24 | 25 | override fun onPostExecute(values: Result) { 26 | super.onPostExecute(values) 27 | onFinish(values) 28 | } 29 | 30 | /** 31 | * Fetches a url and returns what was downloaded or null 32 | * 33 | * @param url to query 34 | */ 35 | private fun getItem(url: String): StringBuilder { 36 | var toReturn: StringBuilder 37 | try { 38 | val response = fetchURL(url).execute().body()?.string() ?: "" 39 | toReturn = StringBuilder(response) 40 | } catch (e: IOException) { //network exception 41 | Timber.w(e,"Could not connect to $url") 42 | toReturn = StringBuilder() 43 | } 44 | 45 | return toReturn 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/utils/AndroidUtils.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer.utils 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | 7 | /** 8 | * Creates an intent to open a link. 9 | * 10 | * @param url The url to be opened. 11 | * @param context The context needed to start the intent. 12 | */ 13 | fun openLink(url: String, context: Context) { 14 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) 15 | context.startActivity(intent) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/utils/NetworkUtils.kt: -------------------------------------------------------------------------------- 1 | 2 | package com.jtmcn.archwiki.viewer.utils 3 | 4 | import okhttp3.Call 5 | import okhttp3.OkHttpClient 6 | import okhttp3.Request 7 | 8 | 9 | 10 | private val client = OkHttpClient.Builder().build() 11 | private val builder = Request.Builder() 12 | 13 | /** 14 | * Fetches a url with optional caching. 15 | * 16 | * @param url url to be fetched. 17 | * @param cb callback to handle result 18 | */ 19 | fun fetchURL(url: String): Call = client.newCall(builder.url(url).build()) 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/jtmcn/archwiki/viewer/utils/SettingsUtils.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer.utils 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import androidx.preference.PreferenceManager 6 | import com.jtmcn.archwiki.viewer.Prefs 7 | 8 | @Deprecated("Use getTextZoom", replaceWith = ReplaceWith("getTextZoom(this)")) 9 | fun getTextSize(prefs: SharedPreferences): Int? { 10 | if(!prefs.contains(Prefs.KEY_TEXT_SIZE)) { 11 | return null 12 | } 13 | 14 | // https://stackoverflow.com/questions/11346916/listpreference-use-string-array-as-entry-and-integer-array-as-entry-values-does 15 | // the value of this preference must be parsed as a string 16 | val fontSizePref = prefs.getString(Prefs.KEY_TEXT_SIZE, "2")!! 17 | return Integer.valueOf(fontSizePref) 18 | 19 | } 20 | 21 | @Deprecated("Use getTextZoom") 22 | fun textSizeToTextZoom(fontSize: Int) = when(fontSize) { 23 | 0 -> 50 24 | 1 -> 75 25 | 2 -> 100 26 | 3 -> 150 27 | 4 -> 200 28 | else -> 100 29 | } 30 | 31 | /** 32 | * gets the [Prefs.KEY_TEXT_ZOOM] preference, and if needed migrates from [getTextSize=] 33 | */ 34 | fun getTextZoom(context: Context): Int { 35 | val prefs = PreferenceManager.getDefaultSharedPreferences(context) 36 | val textSize = getTextSize(prefs) 37 | if(textSize != null) { 38 | val textZoom = textSizeToTextZoom(textSize) 39 | prefs.edit() 40 | .putInt(Prefs.KEY_TEXT_ZOOM, textZoom) 41 | .remove(Prefs.KEY_TEXT_SIZE) 42 | .apply() 43 | 44 | return textZoom 45 | } 46 | 47 | return prefs.getInt(Prefs.KEY_TEXT_ZOOM, 100) 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/play/contact-email.txt: -------------------------------------------------------------------------------- 1 | kevinhinterlong+archwiki@gmail.com -------------------------------------------------------------------------------- /app/src/main/play/contact-website.txt: -------------------------------------------------------------------------------- 1 | https://github.com/kevinhinterlong/archwiki-viewer -------------------------------------------------------------------------------- /app/src/main/play/default-language.txt: -------------------------------------------------------------------------------- 1 | en-US -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/full-description.txt: -------------------------------------------------------------------------------- 1 | A simple viewer for the ArchLinux Wiki online. Page content is formatted for optimal mobile viewing. 2 | 3 | 4 | https://github.com/jtmcn/archwiki-viewer/ -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/feature-graphic/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/play/listings/en-US/graphics/feature-graphic/0.png -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/icon/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/play/listings/en-US/graphics/icon/0.png -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/phone-screenshots/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/play/listings/en-US/graphics/phone-screenshots/0.png -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/phone-screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.png -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/phone-screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.png -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/phone-screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.png -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/phone-screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/play/listings/en-US/graphics/phone-screenshots/4.png -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/short-description.txt: -------------------------------------------------------------------------------- 1 | A simple viewer for the ArchLinux Wiki online. -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/title.txt: -------------------------------------------------------------------------------- 1 | ArchWiki Viewer -------------------------------------------------------------------------------- /app/src/main/play/release-notes/en-US/beta.txt: -------------------------------------------------------------------------------- 1 | v1.0.13 2 | 3 | It's been awhile, here's what's new 4 | - Spanish translation 5 | - Brazilian Portuguese translation 6 | - Slovak translation 7 | 8 | Backend 9 | - Automated releases 10 | - Updated to androidx -------------------------------------------------------------------------------- /app/src/main/play/release-notes/en-US/default.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/play/release-notes/en-US/default.txt -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 9 | 11 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_preferences.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/search_suggestions_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 21 | 22 | 27 | 28 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 11 | 16 | 17 | 20 | 21 | 24 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ArchWiki Viewer 4 | Einstellungen 5 | Link teilen 6 | Suchen 7 | Beenden 8 | Schriftgröße 9 | Aktualisieren 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Visor de ArchWiki 4 | Preferencias 5 | Compartir enlace 6 | Buscar 7 | Salir 8 | Tamaño del texto 9 | Refrescar 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ArchWiki Viewer 4 | Paramètres 5 | Partager le lien 6 | Chercher 7 | Quitter 8 | Taille du texte 9 | Rafraîchir 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-it/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ArchWiki Viewer 4 | Preferenze 5 | Condividi Link 6 | Cerca 7 | Esci 8 | Grandezza testo 9 | Aggiorna 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-iw/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | קורא ArchWiki 4 | העדפות 5 | שתף קישור 6 | חיפוש 7 | Exit 8 | Text Size 9 | Refresh 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt-rBR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ArchWiki Viewer 4 | Preferências 5 | Compartilhar link 6 | Pesquisar 7 | Sair 8 | Tamanho da fonte 9 | Atualizar 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-sk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ArchWiki Viewer 4 | Nastavenia 5 | Zdieľať 6 | Vyhľadať 7 | Ukončiť 8 | Veľkosť textu 9 | Obnoviť 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @color/archBlue 5 | #272727 6 | @color/colorPrimary 7 | 8 | #1793D1 9 | #FFFFFF 10 | #CCCCCC 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @color/archBlue 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ArchWiki Viewer 4 | Preferences 5 | Share Link 6 | Search 7 | Exit 8 | Text Size 9 | Refresh 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/xml/searchable.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/test/java/com/jtmcn/archwiki/viewer/data/SearchResultsBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer.data 2 | 3 | import org.junit.Test 4 | import org.junit.Assert.* 5 | 6 | class SearchResultsBuilderTest { 7 | private val realResult = """["arch", ["Arch-based Distros", "Arch-based distributions", "Arch-chroot", "Arch32", "Arch64 FAQ"], 8 | ["", "", "", "", ""], 9 | ["https://wiki.archlinux.org/index.php/Arch-based_Distros", "https://wiki.archlinux.org/index.php/Arch-based_distributions", "https://wiki.archlinux.org/index.php/Arch-chroot", "https://wiki.archlinux.org/index.php/Arch32", "https://wiki.archlinux.org/index.php/Arch64_FAQ"] 10 | ]""" 11 | 12 | @Test 13 | @Throws(Exception::class) 14 | fun parseSearchResultsTest() { 15 | val searchResults = parseSearchResults(realResult) 16 | assertEquals("Arch-based Distros", searchResults[0].pageName) 17 | assertEquals("https://wiki.archlinux.org/index.php/Arch-based_Distros", searchResults[0].pageURL) 18 | assertEquals("Arch-chroot", searchResults[2].pageName) 19 | assertEquals("https://wiki.archlinux.org/index.php/Arch-chroot", searchResults[2].pageURL) 20 | } 21 | 22 | @Test 23 | fun getSearchQuery() { 24 | val query: String = getSearchQuery("arch") 25 | assertEquals("https://wiki.archlinux.org/api.php?action=opensearch&format=json&formatversion=2&namespace=0&suggest=true&search=arch&limit=10", query) 26 | val queryWithLength: String = getSearchQuery("arch", 9) 27 | assertEquals("https://wiki.archlinux.org/api.php?action=opensearch&format=json&formatversion=2&namespace=0&suggest=true&search=arch&limit=9", queryWithLength) 28 | } 29 | 30 | @Test 31 | @Throws(Exception::class) 32 | fun emptySearchCorrectFormat() { 33 | val fakeResult = "[\"\", [],\n" + 34 | "\t[],\n" + 35 | "\t[]\n" + 36 | "]" 37 | val searchResults = parseSearchResults(fakeResult) 38 | assertEquals(0, searchResults.size) 39 | } 40 | 41 | @Test 42 | @Throws(Exception::class) 43 | fun emptyStringSearch() { 44 | val fakeResult = "" 45 | val searchResults = parseSearchResults(fakeResult) 46 | assertEquals(0, searchResults.size) 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/test/java/com/jtmcn/archwiki/viewer/data/WikiPageBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package com.jtmcn.archwiki.viewer.data 2 | 3 | import com.jtmcn.archwiki.viewer.LOCAL_CSS 4 | import org.junit.Assert.* 5 | 6 | import org.junit.Test 7 | 8 | class WikiPageBuilderTest { 9 | @Test 10 | @Throws(Exception::class) 11 | fun getPageTitle() { 12 | val fakeTitle = "fake title..1!@#!@#!" 13 | assertEquals(fakeTitle, getPageTitle(wrappedTitle(fakeTitle))) 14 | 15 | assertEquals("", getPageTitle(wrappedTitle(""))) 16 | } 17 | 18 | private fun wrappedTitle(fakeTitle: String) = 19 | StringBuilder("${HTML_TITLE_OPEN}${fakeTitle}${HTML_TITLE_CLOSE}") 20 | 21 | @Test 22 | @Throws(Exception::class) 23 | fun getEmptyTitle() { 24 | assertNull(getPageTitle(StringBuilder())) 25 | } 26 | 27 | @Test 28 | @Throws(Exception::class) 29 | fun injectLocalCSS() { 30 | val head = StringBuilder("${HTML_HEAD_OPEN}${HTML_HEAD_CLOSE}") 31 | val passed = injectLocalCSS(head, LOCAL_CSS) 32 | assertTrue(passed) 33 | assertEquals("", 34 | head.toString()) 35 | } 36 | 37 | @Test 38 | @Throws(Exception::class) 39 | fun injectLocalCSSFail() { 40 | val fakeHead = " " 41 | val head = StringBuilder(fakeHead) 42 | val passed = injectLocalCSS(head, LOCAL_CSS) 43 | assertFalse(passed) 44 | assertEquals(fakeHead, head.toString()) 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = '1.3.61' 4 | repositories { 5 | jcenter() 6 | google() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | jcenter() 17 | google() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 | # http://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 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /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 http://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 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /playstore/archwiki_feature_graphic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 55 | 57 | 65 | 69 | 73 | 77 | 81 | 85 | 86 | 95 | 97 | 101 | 105 | 106 | 115 | 116 | 123 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 176 | 181 | 186 | 191 | 196 | Mobile wiki 207 | 208 | 215 | 216 | -------------------------------------------------------------------------------- /playstore/ic_launcher.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 51 | 53 | 61 | 65 | 69 | 73 | 77 | 81 | 82 | 91 | 93 | 97 | 101 | 102 | 111 | 112 | 116 | 121 | 126 | 127 | 131 | 136 | 141 | 142 | 148 | 153 | 154 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /upload_config.tar.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhinterlong/archwiki-viewer/7a9a1c96ec0f33adc46231c3f95c586173361a5d/upload_config.tar.enc --------------------------------------------------------------------------------