├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── dropbox-deployment.yml ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── changelogs │ │ ├── 46.txt │ │ ├── 64.txt │ │ ├── 65.txt │ │ ├── 66.txt │ │ ├── 68.txt │ │ ├── 69.txt │ │ ├── 70.txt │ │ ├── 71.txt │ │ ├── 72.txt │ │ ├── 73.txt │ │ ├── 74.txt │ │ ├── 75.txt │ │ ├── 76.txt │ │ └── 77.txt │ ├── full_description.txt │ ├── images │ │ ├── featureGraphic.png │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── 1_en-US.jpeg │ │ │ ├── 2_en-US.jpeg │ │ │ ├── 3_en-US.jpeg │ │ │ ├── 4_en-US.jpeg │ │ │ ├── 5_en-US.jpeg │ │ │ ├── 6_en-US.jpeg │ │ │ └── 7_en-US.jpeg │ ├── short_description.txt │ ├── title.txt │ └── video.txt │ ├── zh-CN │ ├── changelogs │ │ ├── 46.txt │ │ ├── 64.txt │ │ ├── 65.txt │ │ ├── 66.txt │ │ ├── 68.txt │ │ ├── 69.txt │ │ ├── 70.txt │ │ ├── 71.txt │ │ ├── 72.txt │ │ ├── 73.txt │ │ ├── 74.txt │ │ ├── 75.txt │ │ ├── 76.txt │ │ └── 77.txt │ ├── full_description.txt │ ├── images │ │ └── phoneScreenshots │ │ │ ├── 1_zh-CN.jpeg │ │ │ ├── 2_zh-CN.jpeg │ │ │ ├── 3_zh-CN.jpeg │ │ │ ├── 4_zh-CN.jpeg │ │ │ ├── 5_zh-CN.jpeg │ │ │ ├── 6_zh-CN.jpeg │ │ │ └── 7_zh-CN.jpeg │ ├── short_description.txt │ ├── title.txt │ └── video.txt │ ├── zh-HK │ ├── changelogs │ │ ├── 68.txt │ │ ├── 69.txt │ │ ├── 70.txt │ │ ├── 71.txt │ │ └── 72.txt │ ├── full_description.txt │ ├── short_description.txt │ ├── title.txt │ └── video.txt │ └── zh-TW │ ├── changelogs │ ├── 68.txt │ ├── 69.txt │ ├── 70.txt │ ├── 71.txt │ └── 72.txt │ ├── full_description.txt │ ├── short_description.txt │ ├── title.txt │ └── video.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots └── screenshot-github.jpg ├── secrets.tar.enc ├── settings.gradle └── xkcd ├── .gitignore ├── build.gradle ├── google-services.json ├── lint.xml ├── objectbox-models ├── default.json └── default.json.bak ├── proguard-rules-no-obfuscate.pro ├── proguard-rules.pro ├── resource_mapping.txt └── src ├── androidTest └── java │ └── xyz │ └── jienan │ └── xkcd │ └── OpenDetailComicTest.kt ├── debug ├── AndroidManifest.xml ├── java │ └── xyz │ │ └── jienan │ │ └── xkcd │ │ └── DebugUtils.kt └── res │ ├── menu │ └── menu_main.xml │ └── values │ └── strings.xml ├── foss └── java │ └── xyz │ └── jienan │ └── xkcd │ ├── FlavorUtils.kt │ └── base │ ├── BaseActivity.kt │ └── BaseFragment.kt ├── main ├── AndroidManifest.xml ├── assets │ ├── ImgInterface.js │ ├── LatexInterface.js │ ├── RefInterface.js │ ├── graph_tile.jpg │ ├── night_style.css │ └── style.css ├── java │ └── xyz │ │ └── jienan │ │ └── xkcd │ │ ├── Const.java │ │ ├── XkcdApplication.kt │ │ ├── base │ │ ├── BasePresenter.kt │ │ ├── BaseView.kt │ │ ├── NotificationWorker.kt │ │ ├── glide │ │ │ ├── ExternalSDCacheDiskCacheFactory.kt │ │ │ ├── GlideImageLoader.kt │ │ │ ├── GlideLoaderException.java │ │ │ ├── ImageDownloadTarget.kt │ │ │ ├── ImageFallback.kt │ │ │ ├── MyProgressTarget.java │ │ │ ├── OkHttpProgressGlideModule.java │ │ │ ├── OkHttpProgressResponseBody.java │ │ │ ├── ProgressTarget.java │ │ │ ├── ResponseProgressListener.java │ │ │ ├── WrappingTarget.java │ │ │ └── XkcdGlideUtils.kt │ │ └── network │ │ │ ├── NetworkService.kt │ │ │ ├── QuoteAPI.kt │ │ │ ├── TLSSocketFactory.java │ │ │ ├── WhatIfAPI.kt │ │ │ └── XkcdAPI.kt │ │ ├── comics │ │ ├── ComicsPagerAdapter.kt │ │ ├── SearchCursorAdapter.kt │ │ ├── activity │ │ │ ├── ImageDetailPageActivity.kt │ │ │ └── ImageWebViewActivity.kt │ │ ├── contract │ │ │ ├── ComicsMainContract.java │ │ │ ├── ImageDetailPageContract.kt │ │ │ └── SingleComicContract.kt │ │ ├── dialog │ │ │ ├── NumberPickerDialogFragment.java │ │ │ └── SimpleInfoDialogFragment.kt │ │ ├── fragment │ │ │ ├── ComicsMainFragment.kt │ │ │ └── SingleComicFragment.kt │ │ └── presenter │ │ │ ├── ComicsMainPresenter.kt │ │ │ ├── ImageDetailPagePresenter.kt │ │ │ └── SingleComicPresenter.kt │ │ ├── extra │ │ ├── ExtraPagerAdapter.kt │ │ ├── contract │ │ │ ├── ExtraMainContract.java │ │ │ └── SingleExtraContract.java │ │ ├── fragment │ │ │ ├── ExtraMainFragment.kt │ │ │ ├── SingleExtraFragment.kt │ │ │ └── SingleExtraWebViewFragment.kt │ │ └── presenter │ │ │ ├── ExtraMainPresenter.kt │ │ │ └── SingleExtraPresenter.kt │ │ ├── home │ │ ├── MainActivity.kt │ │ └── base │ │ │ ├── BaseStatePagerAdapter.kt │ │ │ ├── ContentMainBaseFragment.kt │ │ │ └── ContentMainBasePresenter.kt │ │ ├── list │ │ ├── ListBaseAdapter.kt │ │ ├── ListFilterDialogFragment.kt │ │ ├── RecyclerViewFastScroller.kt │ │ ├── WhatIfListAdapter.kt │ │ ├── XkcdListGridAdapter.kt │ │ ├── activity │ │ │ ├── BaseListActivity.kt │ │ │ ├── BaseListView.kt │ │ │ ├── WhatIfListActivity.kt │ │ │ └── XkcdListActivity.kt │ │ ├── contract │ │ │ ├── WhatIfListContract.kt │ │ │ └── XkcdListContract.kt │ │ └── presenter │ │ │ ├── ListPresenter.kt │ │ │ ├── WhatIfListPresenter.kt │ │ │ └── XkcdListPresenter.kt │ │ ├── model │ │ ├── ExtraComics.kt │ │ ├── ExtraModel.kt │ │ ├── Quote.kt │ │ ├── QuoteModel.kt │ │ ├── WhatIfArticle.kt │ │ ├── WhatIfModel.kt │ │ ├── XkcdModel.kt │ │ ├── XkcdPic.kt │ │ ├── persist │ │ │ ├── BoxManager.kt │ │ │ └── SharedPrefManager.kt │ │ ├── util │ │ │ ├── ExplainLinkUtil.kt │ │ │ ├── ExtraHtmlUtil.kt │ │ │ ├── WhatIfArticleUtil.kt │ │ │ ├── XkcdExplainUtil.kt │ │ │ └── XkcdSideloadUtils.kt │ │ └── work │ │ │ ├── WhatIfFastLoadWorker.kt │ │ │ ├── XkcdDownloadWorker.kt │ │ │ └── XkcdFastLoadWorker.kt │ │ ├── settings │ │ ├── ButtonPreference.kt │ │ ├── ManageSpaceActivity.kt │ │ ├── ManageSpaceFragment.kt │ │ ├── PreferenceActivity.kt │ │ └── SettingsFragment.kt │ │ ├── ui │ │ ├── AnimUtils.java │ │ ├── CircleProgressBar.kt │ │ ├── CustomMovementMethod.java │ │ ├── NotificationUtils.kt │ │ ├── Progressable.kt │ │ ├── RecyclerItemClickListener.kt │ │ ├── ToastUtils.kt │ │ ├── UiUtils.kt │ │ ├── WhatIfWebView.kt │ │ ├── like │ │ │ ├── CircleView.kt │ │ │ ├── DotsView.kt │ │ │ ├── Icon.kt │ │ │ ├── IconType.kt │ │ │ ├── LikeButton.kt │ │ │ ├── LikeButtonToggleAnimation.kt │ │ │ ├── LikeUtils.kt │ │ │ └── OnLikeListener.java │ │ └── xkcdimageview │ │ │ ├── BigImageView.kt │ │ │ ├── DragImageView.kt │ │ │ ├── ImageInfoExtractor.kt │ │ │ ├── ImageLoader.kt │ │ │ ├── ImageLoaderFactory.kt │ │ │ └── ImageViewFactory.kt │ │ └── whatif │ │ ├── WhatIfPagerAdapter.kt │ │ ├── contract │ │ └── WhatIfMainContract.kt │ │ ├── fragment │ │ ├── SingleWhatIfFragment.kt │ │ └── WhatIfMainFragment.kt │ │ ├── interfaces │ │ ├── ImgInterface.kt │ │ ├── LatexInterface.kt │ │ └── RefInterface.kt │ │ └── presenter │ │ └── WhatIfMainPresenter.kt └── res │ ├── anim │ ├── fadein.xml │ ├── fadeout.xml │ ├── fadeout_drop.xml │ └── rotate.xml │ ├── drawable-hdpi │ └── ic_notification.png │ ├── drawable-mdpi │ └── ic_notification.png │ ├── drawable-night │ └── what_if_webview_bg.xml │ ├── drawable-v21 │ └── ripple.xml │ ├── drawable-xhdpi │ └── ic_notification.png │ ├── drawable-xxhdpi │ └── ic_notification.png │ ├── drawable │ ├── anim_fast_forward_shake.xml │ ├── anim_fast_rewind_shake.xml │ ├── anim_pause_to_play.xml │ ├── anim_play_to_pause.xml │ ├── graph_tile.jpg │ ├── ic_action_left.xml │ ├── ic_action_right.xml │ ├── ic_action_search.xml │ ├── ic_action_share.xml │ ├── ic_beret.xml │ ├── ic_black_hat.xml │ ├── ic_blondie.xml │ ├── ic_cueball.xml │ ├── ic_fast_forward.xml │ ├── ic_fast_rewind.xml │ ├── ic_filter_list.xml │ ├── ic_hairbun.xml │ ├── ic_hairy.xml │ ├── ic_heart_off.xml │ ├── ic_heart_on.xml │ ├── ic_heart_white.xml │ ├── ic_launcher_foreground.xml │ ├── ic_megan.xml │ ├── ic_menu.xml │ ├── ic_pause.xml │ ├── ic_play_arrow.xml │ ├── ic_ponytail.xml │ ├── ic_sort_calendar_ascending.xml │ ├── ic_sort_calendar_descending.xml │ ├── ic_thumb_off.xml │ ├── ic_thumb_on.xml │ ├── ic_what_if_list.xml │ ├── ic_white_circle_bg.xml │ ├── ic_whitehat.xml │ ├── ic_xkcd_list.xml │ ├── item_num_bg.xml │ ├── recycler_view_fast_scroller_bubble.xml │ ├── recycler_view_fast_scroller_handle.xml │ ├── ripple.xml │ └── what_if_webview_bg.xml │ ├── font │ ├── xkcd.xml │ └── xkcd_script.ttf │ ├── layout-land │ ├── drawer_footer.xml │ └── fragment_comic_single.xml │ ├── layout │ ├── activity_image_detail.xml │ ├── activity_image_webview.xml │ ├── activity_list.xml │ ├── activity_main.xml │ ├── dialog_explain.xml │ ├── dialog_picker.xml │ ├── drawer_footer.xml │ ├── fab_sub_icons.xml │ ├── fragment_comic_main.xml │ ├── fragment_comic_single.xml │ ├── fragment_extra_single.xml │ ├── fragment_what_if_single.xml │ ├── item_filter_dialog.xml │ ├── item_search_suggestion.xml │ ├── item_what_if_list.xml │ ├── item_xkcd_list.xml │ ├── likeview.xml │ ├── nav_header.xml │ ├── pref_widget_delete.xml │ └── rv_scroller.xml │ ├── menu │ ├── menu_drawer.xml │ ├── menu_extra.xml │ ├── menu_list.xml │ ├── menu_main.xml │ ├── menu_what_if.xml │ └── menu_xkcd.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 │ ├── raw │ ├── quotes.json │ ├── xkcd_extra.json │ └── xkcd_special.json │ ├── values-b+zh+hant+TW │ ├── api.xml │ └── strings.xml │ ├── values-b+zh │ ├── api.xml │ └── strings.xml │ ├── values-de │ ├── api.xml │ └── strings.xml │ ├── values-es │ ├── api.xml │ └── strings.xml │ ├── values-fr │ ├── api.xml │ └── strings.xml │ ├── values-night │ └── colors.xml │ ├── values-ru │ ├── api.xml │ └── strings.xml │ ├── values │ ├── api.xml │ ├── array.xml │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── non_translatable.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ ├── backup_descriptor.xml │ ├── network_security_config.xml │ ├── prefs.xml │ ├── prefs_storage.xml │ └── searchable.xml ├── proprietary ├── AndroidManifest.xml └── java │ └── xyz │ └── jienan │ └── xkcd │ ├── FlavorUtils.kt │ └── base │ ├── BaseActivity.kt │ ├── BaseFragment.kt │ └── firebase │ └── XkcdFirebaseMessagingService.kt ├── release ├── java │ └── xyz │ │ └── jienan │ │ └── xkcd │ │ └── DebugUtils.java └── res │ ├── raw │ └── keep.xml │ └── values │ └── debug_placeholder.xml └── test └── java └── xyz └── jienan └── xkcd ├── comics └── activity │ └── ImageWebViewActivityTest.kt └── model └── util └── WhatIfArticleUtilTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | /.idea/ 11 | key.jks 12 | api-[a-z0-9-]*.json 13 | Gemfile 14 | Gemfile.lock 15 | fastlane/Appfile 16 | fastlane/Fastfile 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | dist: trusty 3 | before_cache: 4 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 5 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 6 | cache: 7 | directories: 8 | - "$HOME/.gradle/caches/" 9 | - "$HOME/.gradle/wrapper/" 10 | - "$HOME/.android/build-cache" 11 | android: 12 | components: 13 | - build-tools-29.0.0 14 | - android-29 15 | before_install: 16 | - yes | sdkmanager "platforms;android-29" 17 | - yes | sdkmanager "build-tools;29.0.0" 18 | - yes | sdkmanager "build-tools;28.0.3" 19 | - openssl aes-256-cbc -K $encrypted_89744ce7a44a_key -iv $encrypted_89744ce7a44a_iv -in secrets.tar.enc -out secrets.tar -d 20 | - tar xvf secrets.tar 21 | - ls 22 | #- mv google-services.json xkcd/google-services.json 23 | # - rvm install 2.3.1 24 | # - gem install dropbox-deployment 25 | script: 26 | #- "./gradlew assembleFossRelease" 27 | #- "./gradlew resguardProprietaryRelease" 28 | - "./gradlew assembleRelease" 29 | deploy: 30 | provider: releases 31 | api_key: 32 | secure: 2JNGJxl5VSgZkU4Fuvw8Cj1lulQJX6tMJQbmdZ6lZ4XeG1vI5d3Qt/xLI5s8h/ahXDSFZwA/HefJ2r6MxSSs4sAtpXjMEkyudLEqetbYV/0Y7URmgC2M97YfObsRD4eEM238jOkP/b9SLgzZ9K8MDv2YNhfztG+XT5rtVQcr+Vw3EJ4omvkmtj6MMClCii7fFhCtQOUljusFe+QzdwlbeuPhltO1LJ+TczNmP3A0+f3iHA30TIQAm15NFhbHI8Bd/DmhljLVRMlAEIrk8pAC2dRFURsheVBTQWricyXP/+gdHEtD8HuWZHOlNhs7KiBCgKEMpzI7gedCJWPSv5CNO00iiJtCEj3AlVEjjp5RyNsSM9cvqwRjmZgNH1Cpd6GN9ifjzzjiD9402kk8Cwl9WtEPX0ZZ80kM8fd/Ce+qAVlC3bKnU/4QX8OAWQfqMmXtBdDLGCIQqGW1LD3ntPMu62i0AUzEcdXSTJ0rerpcBWBKTuFvtA2Q5Cm2erOC+NpOSmVSnpKhn/8EHiZDzRXe8VAhaoNrvEgEbSOApzRAgEZ0HHzh6Pcx/2uaPiNgxTJmeo3ccdI1MC5rvOkpkPYgitRdUYILI8a1sCGS3k0BkwEoZxt4jv1ulQHBEaAKVppK24IoAB/PQcBIWqxEonIcnZ3loBinXgPxEEdqQ2qQrhc= 33 | file_glob: true 34 | file: xkcd/build/outputs/apk/*/release/*.apk 35 | skip_cleanup: true 36 | on: 37 | tags: true 38 | all_branches: true 39 | draft: true 40 | after_deploy: 41 | - mkdir -p xkcd/build/outputs/mapping/release/upload 42 | - mv xkcd/build/outputs/mapping/foss/release/mapping.txt "xkcd/build/outputs/mapping/release/upload/mapping_foss_${TRAVIS_TAG:5}.txt" 43 | - mv xkcd/build/outputs/mapping/proprietary/release/mapping.txt "xkcd/build/outputs/mapping/release/upload/mapping_proprietary_${TRAVIS_TAG:5}.txt" 44 | # - dropbox-deployment 45 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.objectboxVersion = '2.9.1' 5 | ext.booster_version = '4.10.0' 6 | ext.isProprietary = getGradle().getStartParameter().getTaskRequests().toString().contains("Proprietary") 7 | repositories { 8 | google() 9 | mavenCentral() 10 | 11 | } 12 | dependencies { 13 | classpath 'com.android.tools.build:gradle:7.3.1' 14 | if (isProprietary) { 15 | classpath 'com.google.gms:google-services:4.3.5' 16 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.2' 17 | // classpath "com.tencent.mm:AndResGuard-gradle-plugin:1.2.21" 18 | } 19 | classpath "io.objectbox:objectbox-gradle-plugin:$objectboxVersion" 20 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10" 21 | // classpath "com.didiglobal.booster:booster-gradle-plugin:$booster_version" 22 | // classpath "com.didiglobal.booster:booster-transform-thread:$booster_version" 23 | // classpath "com.didiglobal.booster:booster-transform-webview:$booster_version" 24 | // classpath "com.didiglobal.booster:booster-transform-shared-preferences:$booster_version" 25 | // classpath "com.didiglobal.booster:booster-transform-r-inline:$booster_version" 26 | // classpath "com.didiglobal.booster:booster-transform-toast:$booster_version" 27 | // classpath "com.didiglobal.booster:booster-transform-activity-thread:$booster_version" 28 | // classpath "com.didiglobal.booster:booster-task-resource-deredundancy:$booster_version" 29 | // classpath "com.didiglobal.booster:booster-task-compression-cwebp:$booster_version" 30 | } 31 | } 32 | 33 | allprojects { 34 | repositories { 35 | google() 36 | mavenCentral() 37 | } 38 | } 39 | 40 | task clean(type: Delete) { 41 | delete rootProject.buildDir 42 | } 43 | -------------------------------------------------------------------------------- /dropbox-deployment.yml: -------------------------------------------------------------------------------- 1 | deploy: 2 | dropbox_path: /Xkcd-Builds # The path to the folder on Dropbox where the files will go 3 | artifacts_path: xkcd/build/outputs/mapping/release/upload # can be a single file, or a path 4 | debug: true # if you want to see more logs -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/46.txt: -------------------------------------------------------------------------------- 1 | - Fix the severe crash that bothered users from version 2.5.3 -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/64.txt: -------------------------------------------------------------------------------- 1 | - New feature: xk3d support to comics No.1 - 880 is added, check that in the up-right menu. 2 | - Click on comic date is an alternative way to jump to random comics. 3 | - Fix what if search when the preference is "Also include article contents that liked or thumb-up-ed" -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/65.txt: -------------------------------------------------------------------------------- 1 | - German translations from xkcDE added to available comics. 2 | - More xkcd characters joined into the side drawer menu 3 | - xk3d support to comics No.1 - 880 is added, check that in the up-right menu. 4 | - Click on comic date is an alternative way to jump to random comics. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/66.txt: -------------------------------------------------------------------------------- 1 | - Some comics will comes with French, Russian, Spanish, and Traditional Chinese translation. 2 | - French xkcd from https://xkcd.lapin.org 3 | - Russian xkcd from https://xkcd.ru 4 | - Spanish xkcd from https://es.xkcd.com 5 | - Traditional Chinese xkcd from https://xkcd.tw 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/68.txt: -------------------------------------------------------------------------------- 1 | - Add interactive support for comic #2288 2 | - Vibration on long click now follows Android system settings 3 | - Fix image loading when high resolution comics not available 4 | - Fix xk3d #826 loaded as interactive comic -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/69.txt: -------------------------------------------------------------------------------- 1 | - Fix compatibility on Android 10 -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/70.txt: -------------------------------------------------------------------------------- 1 | - Add prefetch all comics option, for fast or offline browsing -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/71.txt: -------------------------------------------------------------------------------- 1 | - Add settings option to select external comic cache when there is an SD card 2 | - Add settings option to show comic title text on preview page 3 | - Improve fullscreen comic page with navigation bar, and some other UI tweaks 4 | - Improve cache usage and notification 5 | 6 | Full release note on https://git.io/JTJMS -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/72.txt: -------------------------------------------------------------------------------- 1 | - Support search ISO 8601 date for closest comic 2 | - Support #2445 interactive comic 3 | - Improve #1608 interactive functions 4 | 5 | Full release note on https://git.io/JYyD8 -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/73.txt: -------------------------------------------------------------------------------- 1 | - Fix web links not clickable on Android 11 2 | 3 | Full release note on https://is.gd/AViC7y -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/74.txt: -------------------------------------------------------------------------------- 1 | - Fix compatibility with 2022 what if web content format 2 | - Add reversed list for xkcd and what if list view 3 | - Fix web links not clickable on Android 11 4 | - Support search ISO 8601 date for closest comic 5 | - Support #2445 interactive comic 6 | - Improve #1608 interactive functions 7 | 8 | Full release note on https://is.gd/xkcd_app -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/75.txt: -------------------------------------------------------------------------------- 1 | - Fix compatibility with Android 12 2 | - Fix explainxkcd extraction on xkcd#2408 3 | 4 | Full release note on https://is.gd/xkcd_app -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/76.txt: -------------------------------------------------------------------------------- 1 | - Add storage manage page 2 | - Support interactive comic #2601 3 | - Re-enable interactive comics #1350, #1506, #1975 4 | - Fix what if article's title not shown 5 | 6 | Full release note on https://is.gd/xkcd_app -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/77.txt: -------------------------------------------------------------------------------- 1 | - Support interactive comic #2712, #2765 2 | - Add settings for notification 3 | - Improve extra puzzle #1 experience 4 | 5 | Full release note on https://is.gd/xkcd_app -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Kudos to Randall, glory to all xkcd lovers. 2 | 3 | This is yet another xkcd comics and what if articles viewer, while I'm refining it to become the best one, an easy and lite app with best in-app browsing experience[citation needed]. 4 | 5 | This app has so far: 6 | - the best support to all those large xkcd comics. e.g. No. 657, 1040, and the toughest No. 980. 7 | - native support for interactive comics like 1608, 2198, etc 8 | - the best experience to read what if articles on mobile screen. 9 | - the most fantastic list to browse xkcd archive. 10 | - the quickest and most convenient way to check Explainxkcd, just hold and tap. 11 | - the adorable favorite and thumb-up feature to your beloved comics and articles. 12 | 13 | I am actively improving this app and related services, and it is fully open-sourced for every aspect of code on Github. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Kudos to Randal. Best xkcd and what if viewer for me and all xkcd lovers. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | xkcd - comics viewer -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/video.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/en-US/video.txt -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/46.txt: -------------------------------------------------------------------------------- 1 | - 修复 2.5.3 版本引入的严重问题 2 | - 为 2198 加入的中文版本的互动漫画 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/64.txt: -------------------------------------------------------------------------------- 1 | - 新功能: 漫画 No.1 - 880 加入 xk3d 的支持,请在右上角菜单中查看。 2 | - 点击漫画日期等同于摇一摇,可以随机跳转到其它漫画 3 | - 修复 what if 搜索在选项为"同时搜索已收藏或点赞过的文章的内容"时的异常 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/65.txt: -------------------------------------------------------------------------------- 1 | - xkcd 众角色入驻侧边栏 2 | - 漫画 No.1 - 880 加入 xk3d 的支持,请在右上角菜单中查看。 3 | - 点击漫画日期等同于摇一摇,可以随机跳转到其它漫画 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/66.txt: -------------------------------------------------------------------------------- 1 | - 加入法语、俄语、西班牙语、繁体中文的漫画翻译支持. 2 | - 法语 xkcd 来自 https://xkcd.lapin.org 3 | - 俄语 xkcd 来自 https://xkcd.ru 4 | - 西班牙语 xkcd 来自 https://es.xkcd.com 5 | - 繁体中文 xkcd 来自 https://xkcd.tw 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/68.txt: -------------------------------------------------------------------------------- 1 | - 支持 #2288 篇交互型漫画 2 | - 长按图片震动现遵循系统设置 3 | - 修复部分漫画在无对应高清版本时加载失败的问题 4 | - 修复 #826 篇 xk3d 以交互型漫画打开的问题 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/69.txt: -------------------------------------------------------------------------------- 1 | - 修复与 Android 10 的兼容性 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/70.txt: -------------------------------------------------------------------------------- 1 | - 加入预加载所有漫画选项,以支持快速浏览或离线阅读 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/71.txt: -------------------------------------------------------------------------------- 1 | - 当有 SD 卡插入时,提供更改漫画缓存位置选项 2 | - 加入在预览页显示漫画配文的选项 3 | - 改善使用导航小横条时全屏漫画页的沉浸体验,以及其他一些 UI 改善 4 | - 改善缓存和通知的使用 5 | 6 | 查看完整更新日志 https://git.io/JTJMS -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/72.txt: -------------------------------------------------------------------------------- 1 | - 搜索 ISO 8601 格式的日期可以返回最近的漫画 2 | - 加入 #2445 互动漫画的支持 3 | - 改进 #1608 互动漫画的功能 4 | 5 | 查看完整更新日志 https://git.io/JYyD8 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/73.txt: -------------------------------------------------------------------------------- 1 | - 修复 Android 11 设备上网页链接无法点击的问题 2 | 3 | 查看完整更新日志 https://is.gd/AViC7y -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/74.txt: -------------------------------------------------------------------------------- 1 | - 修复与 2022 新版 what if 页面内容的兼容性 2 | - 加入逆序查看 xkcd 与 what if 列表功能 3 | - 修复 Android 11 设备上网页链接无法点击的问题 4 | - 搜索 ISO 8601 格式的日期可以返回与其时间最接近的漫画 5 | - 加入 #2445 互动漫画的支持 6 | - 改进 #1608 互动漫画的功能 7 | 8 | 查看完整更新日志 https://is.gd/xkcd_app -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/75.txt: -------------------------------------------------------------------------------- 1 | - 修复与 Android 12 的兼容性 2 | - 修复 #2408 的 explainxkcd 提取 3 | 4 | 查看完整更新日志 https://is.gd/xkcd_app -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/76.txt: -------------------------------------------------------------------------------- 1 | - 增加数据管理页面 2 | - 支持交互型漫画 #2601 3 | - 恢复支持交互型漫画 #1350, #1506, #1975 4 | - 修复新加载的 what if 文章不显示标题的问题 5 | 6 | 查看完整更新日志 https://is.gd/xkcd_app -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/changelogs/77.txt: -------------------------------------------------------------------------------- 1 | - 支持交互型漫画 #2712, #2765 2 | - 加入通知设置 3 | - 改善 extra 的 puzzle #1 的体验 4 | 5 | 查看完整更新日志 https://is.gd/xkcd_app -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/full_description.txt: -------------------------------------------------------------------------------- 1 | Kudos to Randall, glory to all xkcd lovers. 2 | 3 | 这是众多 xkcd 漫画和 what if 文章阅读器之一[citation needed],而我正在将它打磨至臻,旨在提供最棒的阅读体验 4 | 5 | 目前这一 xkcd viewer 拥有: 6 | - 对那些大图最佳的支持,比如 No. 657, 1040,以及体量惊人的 No. 980; 7 | - 在手机屏幕上浏览 what if 文章的最好的体验 8 | - 最棒的漫画和文章的列表展示; 9 | - 最方便的 Explainxkcd 查询方式,长按、点击,结束! 10 | - 讨人喜欢的收藏、点赞功能,对你喜欢的漫画请疯狂点赞。 11 | - 互动型漫画原生支持,请打开 1608 开始飞驰,打开 2198 开始投掷。 12 | - 来自 @蜡象汉化组 的中文漫画翻译,请前往 xkcd.in 支持或加入他们。 13 | 14 | 应用和相关服务完全开源,所有的的代码都于Github上可见,欢迎提供意见。 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/images/phoneScreenshots/1_zh-CN.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/zh-CN/images/phoneScreenshots/1_zh-CN.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/images/phoneScreenshots/2_zh-CN.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/zh-CN/images/phoneScreenshots/2_zh-CN.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/images/phoneScreenshots/3_zh-CN.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/zh-CN/images/phoneScreenshots/3_zh-CN.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/images/phoneScreenshots/4_zh-CN.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/zh-CN/images/phoneScreenshots/4_zh-CN.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/images/phoneScreenshots/5_zh-CN.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/zh-CN/images/phoneScreenshots/5_zh-CN.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/images/phoneScreenshots/6_zh-CN.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/zh-CN/images/phoneScreenshots/6_zh-CN.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/images/phoneScreenshots/7_zh-CN.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/zh-CN/images/phoneScreenshots/7_zh-CN.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/short_description.txt: -------------------------------------------------------------------------------- 1 | 为喜爱 xkcd 的人们准备的最棒的阅读器 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/title.txt: -------------------------------------------------------------------------------- 1 | xkcd - 漫画浏览器 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/video.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/zh-CN/video.txt -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-HK/changelogs/68.txt: -------------------------------------------------------------------------------- 1 | - 支持 #2288 篇交互型漫畫 2 | - 長按圖片震動現遵循系統設置 3 | - 修復部分漫畫在無對應高清版本時加載失敗的問題 4 | - 修復 #826 篇 xk3d 以交互型漫畫打開的問題 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-HK/changelogs/69.txt: -------------------------------------------------------------------------------- 1 | - 修復與 Android 10 的兼容性 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-HK/changelogs/70.txt: -------------------------------------------------------------------------------- 1 | - 加入預加載所有漫畫選項,以支持快速瀏覽或離線閱讀 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-HK/changelogs/71.txt: -------------------------------------------------------------------------------- 1 | - 當有 SD 卡插入時,提供更改漫畫緩存位置選項 2 | - 加入在預覽頁顯示漫畫配文的選項 3 | - 改善使用導航小橫條時全屏漫畫頁的沉浸體驗,以及其他一些 UI 改善 4 | - 改善緩存和通知的使用 5 | 6 | 查看完整更新日誌 https://git.io/JTJMS -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-HK/changelogs/72.txt: -------------------------------------------------------------------------------- 1 | - 搜索 ISO 8601 格式的日期可以返回最近的漫畫 2 | - 加入 #2445 互動漫畫的支持 3 | - 改進 #1608 互動漫畫的功能 4 | 5 | Full release note on https://git.io/JYyD8 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-HK/full_description.txt: -------------------------------------------------------------------------------- 1 | Kudos to Randall, glory to all xkcd lovers. 2 | 3 | 這是眾多 xkcd 漫畫和 what if 文章閱讀器之壹[citation needed],而我正在將它打磨至臻,旨在提供最棒的閱讀體驗 4 | 5 | 目前這壹 xkcd viewer 擁有: 6 | - 對那些大圖最佳的支持,比如 No. 657, 1040,以及體量驚人的 No. 980; 7 | - 在手機屏幕上瀏覽 what if 文章的最好的體驗 8 | - 最棒的漫畫和文章的列表展示; 9 | - 最方便的 Explainxkcd 查詢方式,長按、點擊,結束! 10 | - 討人喜歡的收藏、點贊功能,對妳喜歡的漫畫請瘋狂點贊。 11 | - 互動型漫畫原生支持,請打開 1608 開始飛馳,打開 2198 開始投擲。 12 | - 來自 @蠟象漢化組 的中文漫畫翻譯,請前往 xkcd.in 支持或加入他們。 13 | 14 | 應用和相關服務完全開源,所有的的代碼都於Github上可見,歡迎提供意見。 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-HK/short_description.txt: -------------------------------------------------------------------------------- 1 | 為喜愛 xkcd 的人們準備的最棒的閱讀器 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-HK/title.txt: -------------------------------------------------------------------------------- 1 | xkcd - 漫畫瀏覽器 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-HK/video.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/zh-HK/video.txt -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-TW/changelogs/68.txt: -------------------------------------------------------------------------------- 1 | - 支持 #2288 篇交互型漫畫 2 | - 長按圖片震動現遵循系統設置 3 | - 修復部分漫畫在無對應高清版本時加載失敗的問題 4 | - 修復 #826 篇 xk3d 以交互型漫畫打開的問題 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-TW/changelogs/69.txt: -------------------------------------------------------------------------------- 1 | - 修復與 Android 10 的兼容性 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-TW/changelogs/70.txt: -------------------------------------------------------------------------------- 1 | - 加入預加載所有漫畫選項,以支持快速瀏覽或離線閱讀 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-TW/changelogs/71.txt: -------------------------------------------------------------------------------- 1 | - 當有 SD 卡插入時,提供更改漫畫緩存位置選項 2 | - 加入在預覽頁顯示漫畫配文的選項 3 | - 改善使用導航小橫條時全屏漫畫頁的沉浸體驗,以及其他一些 UI 改善 4 | - 改善緩存和通知的使用 5 | 6 | 查看完整更新日誌 https://git.io/JTJMS -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-TW/changelogs/72.txt: -------------------------------------------------------------------------------- 1 | - 搜索 ISO 8601 格式的日期可以返回最近的漫畫 2 | - 加入 #2445 互動漫畫的支持 3 | - 改進 #1608 互動漫畫的功能 4 | 5 | Full release note on https://git.io/JYyD8 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-TW/full_description.txt: -------------------------------------------------------------------------------- 1 | Kudos to Randall, glory to all xkcd lovers. 2 | 3 | 這是眾多 xkcd 漫畫和 what if 文章閱讀器之壹[citation needed],而我正在將它打磨至臻,旨在提供最棒的閱讀體驗 4 | 5 | 目前這壹 xkcd viewer 擁有: 6 | - 對那些大圖最佳的支持,比如 No. 657, 1040,以及體量驚人的 No. 980; 7 | - 在手機屏幕上瀏覽 what if 文章的最好的體驗 8 | - 最棒的漫畫和文章的列表展示; 9 | - 最方便的 Explainxkcd 查詢方式,長按、點擊,結束! 10 | - 討人喜歡的收藏、點贊功能,對妳喜歡的漫畫請瘋狂點贊。 11 | - 互動型漫畫原生支持,請打開 1608 開始飛馳,打開 2198 開始投擲。 12 | - 來自 @蠟象漢化組 的中文漫畫翻譯,請前往 xkcd.in 支持或加入他們。 13 | 14 | 應用和相關服務完全開源,所有的的代碼都於Github上可見,歡迎提供意見。 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-TW/short_description.txt: -------------------------------------------------------------------------------- 1 | 为喜爱 xkcd 的人们准备的最棒的阅读器 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-TW/title.txt: -------------------------------------------------------------------------------- 1 | xkcd - 漫畫瀏覽器 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-TW/video.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/fastlane/metadata/android/zh-TW/video.txt -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ## Project-wide Gradle settings. 2 | # 3 | # For more details on how to configure your build environment visit 4 | # http://www.gradle.org/docs/current/userguide/build_environment.html 5 | # 6 | # Specifies the JVM arguments used for the daemon process. 7 | # The setting is particularly useful for tweaking memory settings. 8 | # Default value: -Xmx1024m -XX:MaxPermSize=256m 9 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 10 | # 11 | # When configured, Gradle will run in incubating parallel mode. 12 | # This option should only be used with decoupled projects. More details, visit 13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 14 | # org.gradle.parallel=true 15 | #Fri Jul 07 00:04:56 CST 2017 16 | org.gradle.jvmargs=-Xmx3072m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 17 | #org.gradle.jvmargs=-Xmx1536m 18 | android.useAndroidX=true 19 | android.enableJetifier=true 20 | android.debug.obsoleteApi=true 21 | org.gradle.parallel=true 22 | org.gradle.configureondemand=true 23 | org.gradle.caching=true 24 | android.enableBuildScriptClasspathCheck=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jul 10 00:30:56 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /screenshots/screenshot-github.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/screenshots/screenshot-github.jpg -------------------------------------------------------------------------------- /secrets.tar.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/secrets.tar.enc -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':xkcd' 2 | -------------------------------------------------------------------------------- /xkcd/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /google-services.json -------------------------------------------------------------------------------- /xkcd/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "555415708408", 4 | "firebase_url": "https://xkcd-233.firebaseio.com", 5 | "project_id": "xkcd-233", 6 | "storage_bucket": "xkcd-233.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:555415708408:android:d1d7b448c45d41bb", 12 | "android_client_info": { 13 | "package_name": "xyz.jienan.xkcd" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "555415708408-2c47ugj3qp2egh0opnk6m4jgk8grjhuk.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyAd6bmENN46kf5BPAlTCn5QEoaZVFx3MPk" 25 | } 26 | ], 27 | "services": { 28 | "analytics_service": { 29 | "status": 1 30 | }, 31 | "appinvite_service": { 32 | "status": 1, 33 | "other_platform_oauth_client": [] 34 | }, 35 | "ads_service": { 36 | "status": 2 37 | } 38 | } 39 | }, 40 | { 41 | "client_info": { 42 | "mobilesdk_app_id": "1:555415708408:android:47b48bc03d6bbf02", 43 | "android_client_info": { 44 | "package_name": "xyz.jienan.xkcd.debug" 45 | } 46 | }, 47 | "oauth_client": [ 48 | { 49 | "client_id": "555415708408-2c47ugj3qp2egh0opnk6m4jgk8grjhuk.apps.googleusercontent.com", 50 | "client_type": 3 51 | } 52 | ], 53 | "api_key": [ 54 | { 55 | "current_key": "AIzaSyAd6bmENN46kf5BPAlTCn5QEoaZVFx3MPk" 56 | } 57 | ], 58 | "services": { 59 | "analytics_service": { 60 | "status": 1 61 | }, 62 | "appinvite_service": { 63 | "status": 1, 64 | "other_platform_oauth_client": [] 65 | }, 66 | "ads_service": { 67 | "status": 2 68 | } 69 | } 70 | } 71 | ], 72 | "configuration_version": "1" 73 | } -------------------------------------------------------------------------------- /xkcd/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /xkcd/proguard-rules-no-obfuscate.pro: -------------------------------------------------------------------------------- 1 | -dontobfuscate -------------------------------------------------------------------------------- /xkcd/resource_mapping.txt: -------------------------------------------------------------------------------- 1 | res path mapping: 2 | res/font -> res/font 3 | res/font-v22 -> res/font-v22 -------------------------------------------------------------------------------- /xkcd/src/androidTest/java/xyz/jienan/xkcd/OpenDetailComicTest.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd 2 | 3 | import androidx.test.espresso.Espresso.onView 4 | import androidx.test.espresso.action.ViewActions.click 5 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 6 | import androidx.test.espresso.matcher.ViewMatchers.withId 7 | import androidx.test.ext.junit.runners.AndroidJUnit4 8 | import androidx.test.rule.ActivityTestRule 9 | import org.hamcrest.Matchers.allOf 10 | import org.junit.Rule 11 | import org.junit.Test 12 | import org.junit.runner.RunWith 13 | import xyz.jienan.xkcd.home.MainActivity 14 | 15 | @RunWith(AndroidJUnit4::class) 16 | class OpenDetailComicTest { 17 | 18 | @get:Rule 19 | var activityRule: ActivityTestRule = ActivityTestRule(MainActivity::class.java) 20 | 21 | @Test 22 | fun open_detail_comic() { 23 | onView(allOf(isDisplayed(), withId(R.id.ivXkcdPic))).perform(click()) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /xkcd/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /xkcd/src/debug/java/xyz/jienan/xkcd/DebugUtils.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.gu.toolargetool.TooLargeTool.startLogging 6 | import timber.log.Timber 7 | 8 | object DebugUtils { 9 | fun init(): Boolean { 10 | if (BuildConfig.DEBUG) { 11 | Timber.plant(Timber.DebugTree()) 12 | } 13 | return true 14 | } 15 | 16 | fun debugDB(context: Context?) { 17 | if (BuildConfig.DEBUG) { 18 | startLogging((context as Application?)!!) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /xkcd/src/debug/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 17 | 18 | 23 | 24 | 29 | 30 | 35 | 36 | 41 | 42 | -------------------------------------------------------------------------------- /xkcd/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | xkcd debug 3 | 4 | Test Notification 5 | 6 | xkcd-Leak Canary 7 | 8 | -------------------------------------------------------------------------------- /xkcd/src/foss/java/xyz/jienan/xkcd/FlavorUtils.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd 2 | 3 | import android.app.Application 4 | import java.util.concurrent.TimeUnit 5 | 6 | object FlavorUtils { 7 | fun init() { 8 | } 9 | 10 | fun updateLocale() { 11 | } 12 | 13 | fun getGmsAvailability(@Suppress("UNUSED_PARAMETER") app: Application) = false 14 | } -------------------------------------------------------------------------------- /xkcd/src/foss/java/xyz/jienan/xkcd/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base 2 | 3 | import android.content.SharedPreferences 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.preference.PreferenceManager 7 | import xyz.jienan.xkcd.Const.PREF_FONT 8 | import xyz.jienan.xkcd.R 9 | import xyz.jienan.xkcd.comics.activity.ImageDetailPageActivity 10 | import xyz.jienan.xkcd.home.MainActivity 11 | 12 | /** 13 | * Created by Jienan on 2018/3/9. 14 | */ 15 | 16 | abstract class BaseActivity : AppCompatActivity() { 17 | 18 | protected val sharedPreferences: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) } 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | setTheme() 22 | super.onCreate(savedInstanceState) 23 | } 24 | 25 | @Suppress("UNUSED_PARAMETER") 26 | protected fun logUXEvent(event: String, bundle: Bundle? = null) { 27 | // analytics disabled 28 | } 29 | 30 | private fun setTheme() { 31 | val fontPref = sharedPreferences.getBoolean(PREF_FONT, false) 32 | if (fontPref) { 33 | when (this) { 34 | is MainActivity -> setTheme(R.style.CustomActionBarTheme) 35 | is ImageDetailPageActivity -> setTheme(R.style.TransparentBackgroundTheme) 36 | else -> setTheme(R.style.AppBarTheme) 37 | } 38 | } else { 39 | when (this) { 40 | is MainActivity -> setTheme(R.style.CustomActionBarFontTheme) 41 | is ImageDetailPageActivity -> setTheme(R.style.TransparentBackgroundTheme) 42 | else -> setTheme(R.style.AppBarFontTheme) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /xkcd/src/foss/java/xyz/jienan/xkcd/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.annotation.LayoutRes 8 | import androidx.fragment.app.Fragment 9 | 10 | abstract class BaseFragment : Fragment() { 11 | 12 | @get:LayoutRes 13 | protected abstract val layoutResId: Int 14 | 15 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = 16 | inflater.inflate(layoutResId, container, false) 17 | 18 | @Suppress("UNUSED_PARAMETER") 19 | protected fun logUXEvent(event: String, params: Map? = null) { 20 | // analytics disabled 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /xkcd/src/main/assets/ImgInterface.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load",function(){Array.from(document.getElementsByClassName("illustration")).forEach(function(t,e,n){function s(e){null!==r&&(clearTimeout(r),r=null),this.classList.remove("longpress")}function o(e){if("click"!==e.type||0===e.button)return null!=e.touches&&(u=e.touches[0].screenX,a=e.touches[0].screenY,l=window.visualViewport.scale),c=!1,this.classList.add("longpress"),null===r&&(r=setTimeout(function(){c=!0,AndroidImg.doLongPress(t.title)},500)),!1}function i(e){if(null!=e.touches){var t=e.touches[0].screenX,n=e.touches[0].screenY,s=Math.abs(u-t),o=Math.abs(a-n);20n.scrollLeft?AndroidLatex.onTouch(2):AndroidLatex.onTouch(3)},!1),n.addEventListener("touchend",function(o){AndroidLatex.onTouch(3)},!1)})},!1); -------------------------------------------------------------------------------- /xkcd/src/main/assets/RefInterface.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load",function(){Array.from(document.getElementsByClassName("refnum")).forEach(function(e,n,t){e.addEventListener("click",function(n){AndroidRef.refContent(e.nextSibling.innerHTML)},!1)})},!1); -------------------------------------------------------------------------------- /xkcd/src/main/assets/graph_tile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/assets/graph_tile.jpg -------------------------------------------------------------------------------- /xkcd/src/main/assets/night_style.css: -------------------------------------------------------------------------------- 1 | body{background:#3a3a3a}blockquote,body ol li,body ul li,div,p,tbody tr td{color:#ebebeb}#attribute,#question,.refnum,a:link,sup span{color:#add8e6!important}.illustration{background:#aaa} -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/XkcdApplication.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd 2 | 3 | import android.app.Application 4 | import android.content.res.Configuration 5 | import androidx.appcompat.app.AppCompatDelegate 6 | import androidx.preference.PreferenceManager 7 | import timber.log.Timber 8 | import xyz.jienan.xkcd.base.glide.GlideImageLoader 9 | import xyz.jienan.xkcd.model.MyObjectBox 10 | import xyz.jienan.xkcd.model.XkcdModel 11 | import xyz.jienan.xkcd.model.persist.BoxManager 12 | import xyz.jienan.xkcd.model.persist.SharedPrefManager 13 | import xyz.jienan.xkcd.ui.xkcdimageview.ImageLoaderFactory 14 | 15 | /** 16 | * Created by Jienan on 2018/3/2. 17 | */ 18 | 19 | class XkcdApplication : Application(), androidx.work.Configuration.Provider { 20 | 21 | var gmsAvailability = false 22 | 23 | override fun onCreate() { 24 | super.onCreate() 25 | if (!DebugUtils.init()) { 26 | return 27 | } 28 | FlavorUtils.init() 29 | updateLocale() 30 | AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) 31 | AppCompatDelegate.setDefaultNightMode((PreferenceManager 32 | .getDefaultSharedPreferences(this) 33 | .getString("pref_dark", "1") ?: "1") 34 | .toInt()) 35 | instance = this 36 | val boxStore = MyObjectBox.builder().androidContext(this).maxReaders(300).build() 37 | DebugUtils.debugDB(this) 38 | BoxManager.init(boxStore) 39 | SharedPrefManager.init(this) 40 | 41 | ImageLoaderFactory.initialize(GlideImageLoader.with(this)) 42 | gmsAvailability = FlavorUtils.getGmsAvailability(this) 43 | Timber.d("GMS availability $gmsAvailability") 44 | } 45 | 46 | companion object { 47 | 48 | var instance: XkcdApplication? = null 49 | private set 50 | } 51 | 52 | override fun onConfigurationChanged(newConfig: Configuration) { 53 | super.onConfigurationChanged(newConfig) 54 | updateLocale() 55 | } 56 | 57 | private fun updateLocale() { 58 | XkcdModel.localizedUrl = resources.getString(R.string.api_xkcd_localization) 59 | FlavorUtils.updateLocale() 60 | } 61 | 62 | override fun getWorkManagerConfiguration(): androidx.work.Configuration { 63 | return androidx.work.Configuration.Builder() 64 | .setMinimumLoggingLevel(android.util.Log.INFO) 65 | .build() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/BasePresenter.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base 2 | 3 | interface BasePresenter { 4 | 5 | fun onDestroy() 6 | 7 | } 8 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/BaseView.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base 2 | 3 | interface BaseView 4 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/NotificationWorker.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base 2 | 3 | import android.content.Context 4 | import androidx.core.app.NotificationManagerCompat 5 | import androidx.preference.PreferenceManager 6 | import androidx.work.RxWorker 7 | import androidx.work.WorkerParameters 8 | import io.reactivex.Completable 9 | import io.reactivex.Single 10 | import io.reactivex.schedulers.Schedulers 11 | import timber.log.Timber 12 | import xyz.jienan.xkcd.Const 13 | import xyz.jienan.xkcd.base.network.NetworkService 14 | import xyz.jienan.xkcd.model.XkcdPic 15 | import xyz.jienan.xkcd.model.persist.BoxManager 16 | import xyz.jienan.xkcd.model.persist.SharedPrefManager 17 | import xyz.jienan.xkcd.ui.NotificationUtils 18 | 19 | class NotificationWorker(appContext: Context, workerParams: WorkerParameters) 20 | : RxWorker(appContext, workerParams) { 21 | 22 | override fun createWork(): Single { 23 | Timber.d("Create work ${this.tags}") 24 | return NetworkService.xkcdAPI 25 | .latest 26 | .observeOn(Schedulers.io()) 27 | .doOnNext { xkcdPic -> Timber.d("GetXkcdPic $xkcdPic") } 28 | .flatMapCompletable { checkLatestPicAndNotify(it) } 29 | .toSingleDefault(Result.success()) 30 | .onErrorReturnItem(Result.failure()) 31 | } 32 | 33 | private fun checkLatestPicAndNotify(xkcdPic: XkcdPic) : Completable { 34 | return Completable.fromAction { 35 | if (SharedPrefManager.latestXkcd < xkcdPic.num) { 36 | SharedPrefManager.latestXkcd = xkcdPic.num 37 | BoxManager.updateAndSave(xkcdPic) 38 | val allowNotification = PreferenceManager 39 | .getDefaultSharedPreferences(applicationContext) 40 | .getBoolean(Const.PREF_NOTIFICATION, true) 41 | && NotificationManagerCompat.from(applicationContext).areNotificationsEnabled() 42 | if (allowNotification) { 43 | NotificationUtils.showNotification(applicationContext, xkcdPic) 44 | } 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/glide/ExternalSDCacheDiskCacheFactory.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base.glide 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.os.Environment 6 | import androidx.annotation.RequiresApi 7 | import com.bumptech.glide.load.engine.cache.DiskLruCacheFactory 8 | import com.bumptech.glide.load.engine.cache.DiskLruCacheFactory.CacheDirectoryGetter 9 | import timber.log.Timber 10 | import java.io.File 11 | 12 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 13 | class ExternalSDCacheDiskCacheFactory @JvmOverloads constructor(context: Context, diskCacheName: String? = DEFAULT_DISK_CACHE_DIR, diskCacheSize: Int = DEFAULT_DISK_CACHE_SIZE) : 14 | DiskLruCacheFactory(CacheDirectoryGetter { 15 | 16 | val file = try { 17 | context.externalCacheDirs.firstOrNull { 18 | try { 19 | !Environment.isExternalStorageEmulated(it) && Environment.isExternalStorageRemovable(it) 20 | } catch (e: Exception) { 21 | Timber.w(e, "Failed to check external storage") 22 | false 23 | } 24 | } 25 | } catch (e: Exception) { 26 | Timber.w(e, "Failed to check external storage") 27 | null 28 | } 29 | 30 | val cacheDirectory = file ?: context.cacheDir ?: null 31 | 32 | if (diskCacheName != null) { 33 | Timber.d("Use cache directory $cacheDirectory") 34 | File(cacheDirectory, diskCacheName) 35 | } 36 | cacheDirectory 37 | }, diskCacheSize) { 38 | constructor(context: Context, diskCacheSize: Int) : this(context, DEFAULT_DISK_CACHE_DIR, diskCacheSize) 39 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/glide/GlideLoaderException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2017 Piasy 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package xyz.jienan.xkcd.base.glide; 26 | 27 | import android.graphics.drawable.Drawable; 28 | 29 | /** 30 | * Created by Piasy{github.com/Piasy} on 03/10/2017. 31 | */ 32 | 33 | public class GlideLoaderException extends RuntimeException { 34 | private final Drawable mErrorDrawable; 35 | 36 | public GlideLoaderException(final Drawable errorDrawable) { 37 | mErrorDrawable = errorDrawable; 38 | } 39 | 40 | public Drawable getErrorDrawable() { 41 | return mErrorDrawable; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/glide/ImageDownloadTarget.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base.glide 2 | 3 | import android.graphics.drawable.Drawable 4 | 5 | import com.bumptech.glide.request.animation.GlideAnimation 6 | import com.bumptech.glide.request.target.SimpleTarget 7 | 8 | import java.io.File 9 | 10 | /** 11 | * Created by Piasy{github.com/Piasy} on 12/11/2016. 12 | */ 13 | 14 | abstract class ImageDownloadTarget protected constructor(private val mUrl: String) : SimpleTarget(), OkHttpProgressGlideModule.UIProgressListener { 15 | 16 | override fun onResourceReady(resource: File, glideAnimation: GlideAnimation) { 17 | OkHttpProgressGlideModule.forget(mUrl) 18 | } 19 | 20 | override fun onLoadCleared(placeholder: Drawable?) { 21 | super.onLoadCleared(placeholder) 22 | OkHttpProgressGlideModule.forget(mUrl) 23 | } 24 | 25 | override fun onLoadStarted(placeholder: Drawable?) { 26 | super.onLoadStarted(placeholder) 27 | OkHttpProgressGlideModule.expect(mUrl, this) 28 | } 29 | 30 | override fun onLoadFailed(e: Exception?, errorDrawable: Drawable?) { 31 | super.onLoadFailed(e, errorDrawable) 32 | OkHttpProgressGlideModule.forget(mUrl) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/glide/ImageFallback.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base.glide 2 | 3 | fun String.fallback(): String { 4 | return when { 5 | startsWith("https") -> { 6 | replaceFirst("https".toRegex(), "http") 7 | } 8 | contains("_2x.") -> { 9 | val indexOf2x = lastIndexOf("_2x.") 10 | if (indexOf2x > 0) { 11 | replaceRange(indexOf2x, indexOf2x + 3, "") 12 | .replaceFirst("http://", "https://") 13 | } else { 14 | this 15 | } 16 | } 17 | else -> this 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/glide/MyProgressTarget.java: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base.glide; 2 | 3 | import android.view.View; 4 | import android.view.animation.AnimationUtils; 5 | import android.widget.ImageView; 6 | 7 | import com.bumptech.glide.request.target.Target; 8 | 9 | import xyz.jienan.xkcd.R; 10 | import xyz.jienan.xkcd.ui.IProgressbar; 11 | 12 | import static android.view.View.VISIBLE; 13 | 14 | public class MyProgressTarget extends ProgressTarget { 15 | private final IProgressbar progressbar; 16 | private final ImageView image; 17 | 18 | public MyProgressTarget(Target target, IProgressbar progress, ImageView image) { 19 | super(target); 20 | this.progressbar = progress; 21 | this.image = image; 22 | } 23 | 24 | @Override 25 | public float getGranualityPercentage() { 26 | return 0.1f; // this matches the format string for #text below 27 | } 28 | 29 | @Override 30 | public void onDownloadStart() { 31 | 32 | } 33 | 34 | @Override 35 | public void onProgress(int progress) { 36 | 37 | } 38 | 39 | @Override 40 | public void onDownloadFinish() { 41 | 42 | } 43 | 44 | @Override 45 | protected void onConnecting() { 46 | progressbar.setProgress(1); 47 | progressbar.setVisibility(VISIBLE); 48 | image.setImageLevel(0); 49 | } 50 | 51 | @Override 52 | protected void onDownloading(long bytesRead, long expectedLength) { 53 | int progress = (int) (100 * bytesRead / expectedLength); 54 | progress = progress <= 0 ? 1 : progress; 55 | progressbar.setProgress(progress); 56 | if (progressbar.getAnimation() == null || !progressbar.getAnimation().hasStarted()) { 57 | progressbar.startAnimation(AnimationUtils.loadAnimation(image.getContext(), R.anim.rotate)); 58 | } 59 | image.setImageLevel((int) (10000 * bytesRead / expectedLength)); 60 | } 61 | 62 | @Override 63 | protected void onDownloaded() { 64 | image.setImageLevel(10000); 65 | } 66 | 67 | @Override 68 | protected void onDelivered() { 69 | progressbar.setVisibility(View.INVISIBLE); 70 | progressbar.clearAnimation(); 71 | image.setImageLevel(0); // reset ImageView default 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/glide/OkHttpProgressResponseBody.java: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base.glide; 2 | 3 | import java.io.IOException; 4 | 5 | import okhttp3.HttpUrl; 6 | import okhttp3.MediaType; 7 | import okhttp3.ResponseBody; 8 | import okio.Buffer; 9 | import okio.BufferedSource; 10 | import okio.ForwardingSource; 11 | import okio.Okio; 12 | import okio.Source; 13 | 14 | /** 15 | * Created by Jienan on 2018/3/7. 16 | */ 17 | 18 | class OkHttpProgressResponseBody extends ResponseBody { 19 | private final HttpUrl url; 20 | private final ResponseBody responseBody; 21 | private final ResponseProgressListener progressListener; 22 | private BufferedSource bufferedSource; 23 | 24 | OkHttpProgressResponseBody(HttpUrl url, ResponseBody responseBody, 25 | ResponseProgressListener progressListener) { 26 | this.url = url; 27 | this.responseBody = responseBody; 28 | this.progressListener = progressListener; 29 | } 30 | 31 | @Override 32 | public MediaType contentType() { 33 | return responseBody.contentType(); 34 | } 35 | 36 | @Override 37 | public long contentLength() { 38 | return responseBody.contentLength(); 39 | } 40 | 41 | @Override 42 | public BufferedSource source() { 43 | if (bufferedSource == null) { 44 | bufferedSource = Okio.buffer(source(responseBody.source())); 45 | } 46 | return bufferedSource; 47 | } 48 | 49 | private Source source(Source source) { 50 | return new ForwardingSource(source) { 51 | long totalBytesRead = 0L; 52 | 53 | @Override 54 | public long read(Buffer sink, long byteCount) throws IOException { 55 | long bytesRead = super.read(sink, byteCount); 56 | long fullLength = responseBody.contentLength(); 57 | if (bytesRead == -1) { // this source is exhausted 58 | totalBytesRead = fullLength; 59 | } else { 60 | totalBytesRead += bytesRead; 61 | } 62 | progressListener.update(url, totalBytesRead, fullLength); 63 | return bytesRead; 64 | } 65 | }; 66 | } 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/glide/ResponseProgressListener.java: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base.glide; 2 | 3 | import okhttp3.HttpUrl; 4 | 5 | /** 6 | * Created by Jienan on 2018/3/7. 7 | */ 8 | 9 | interface ResponseProgressListener { 10 | void update(HttpUrl url, long bytesRead, long contentLength); 11 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/glide/WrappingTarget.java: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base.glide; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | import com.bumptech.glide.request.Request; 6 | import com.bumptech.glide.request.animation.GlideAnimation; 7 | import com.bumptech.glide.request.target.SizeReadyCallback; 8 | import com.bumptech.glide.request.target.Target; 9 | 10 | /** 11 | * Created by jienanzhang on 03/03/2018. 12 | */ 13 | 14 | class WrappingTarget implements Target { 15 | private final Target target; 16 | 17 | WrappingTarget(Target target) { 18 | this.target = target; 19 | } 20 | 21 | @Override 22 | public void getSize(SizeReadyCallback cb) { 23 | target.getSize(cb); 24 | } 25 | 26 | @Override 27 | public void onLoadStarted(Drawable placeholder) { 28 | target.onLoadStarted(placeholder); 29 | } 30 | 31 | @Override 32 | public void onLoadFailed(Exception e, Drawable errorDrawable) { 33 | target.onLoadFailed(e, errorDrawable); 34 | } 35 | 36 | @Override 37 | public void onResourceReady(Z resource, GlideAnimation glideAnimation) { 38 | target.onResourceReady(resource, glideAnimation); 39 | } 40 | 41 | @Override 42 | public void onLoadCleared(Drawable placeholder) { 43 | target.onLoadCleared(placeholder); 44 | } 45 | 46 | @Override 47 | public Request getRequest() { 48 | return target.getRequest(); 49 | } 50 | 51 | @Override 52 | public void setRequest(Request request) { 53 | target.setRequest(request); 54 | } 55 | 56 | @Override 57 | public void onStart() { 58 | target.onStart(); 59 | } 60 | 61 | @Override 62 | public void onStop() { 63 | target.onStop(); 64 | } 65 | 66 | @Override 67 | public void onDestroy() { 68 | target.onDestroy(); 69 | } 70 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/network/QuoteAPI.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base.network 2 | 3 | 4 | import io.reactivex.Observable 5 | import retrofit2.http.GET 6 | import retrofit2.http.Headers 7 | import xyz.jienan.xkcd.model.Quote 8 | 9 | interface QuoteAPI { 10 | 11 | @get:Headers("$HEADER_CACHEABLE: 86400") 12 | @get:GET(QUOTE_LIST) 13 | val quotes: Observable> 14 | } 15 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/network/TLSSocketFactory.java: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base.network; 2 | 3 | import java.io.IOException; 4 | import java.net.InetAddress; 5 | import java.net.Socket; 6 | import java.security.KeyManagementException; 7 | import java.security.NoSuchAlgorithmException; 8 | 9 | import javax.net.ssl.SSLContext; 10 | import javax.net.ssl.SSLSocket; 11 | import javax.net.ssl.SSLSocketFactory; 12 | 13 | /** 14 | * Socket factory enabling TLS 15 | *

16 | * Source: http://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/ 17 | */ 18 | public class TLSSocketFactory extends SSLSocketFactory { 19 | private SSLSocketFactory delegate; 20 | 21 | public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException { 22 | SSLContext context = SSLContext.getInstance("TLS"); 23 | context.init(null, null, null); 24 | delegate = context.getSocketFactory(); 25 | } 26 | 27 | @Override 28 | public String[] getDefaultCipherSuites() { 29 | return delegate.getDefaultCipherSuites(); 30 | } 31 | 32 | @Override 33 | public String[] getSupportedCipherSuites() { 34 | return delegate.getSupportedCipherSuites(); 35 | } 36 | 37 | @Override 38 | public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { 39 | return enableTLSOnSocket(delegate.createSocket(s, host, port, autoClose)); 40 | } 41 | 42 | @Override 43 | public Socket createSocket(String host, int port) throws IOException { 44 | return enableTLSOnSocket(delegate.createSocket(host, port)); 45 | } 46 | 47 | @Override 48 | public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { 49 | return enableTLSOnSocket(delegate.createSocket(host, port, localHost, localPort)); 50 | } 51 | 52 | @Override 53 | public Socket createSocket(InetAddress host, int port) throws IOException { 54 | return enableTLSOnSocket(delegate.createSocket(host, port)); 55 | } 56 | 57 | @Override 58 | public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { 59 | return enableTLSOnSocket(delegate.createSocket(address, port, localAddress, localPort)); 60 | } 61 | 62 | private Socket enableTLSOnSocket(Socket socket) { 63 | if (socket != null && (socket instanceof SSLSocket)) { 64 | ((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.1", "TLSv1.2"}); 65 | } 66 | return socket; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/network/WhatIfAPI.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base.network 2 | 3 | 4 | import io.reactivex.Single 5 | import okhttp3.ResponseBody 6 | import retrofit2.http.* 7 | import xyz.jienan.xkcd.model.WhatIfArticle 8 | 9 | interface WhatIfAPI { 10 | 11 | @get:Headers("$HEADER_CACHEABLE: 600") 12 | @get:GET("archive/") 13 | val archive: Single 14 | 15 | @GET("{article_id}/") 16 | fun getArticle(@Path("article_id") id: Long): Single 17 | 18 | @FormUrlEncoded 19 | @POST 20 | fun thumbsUpWhatIf(@Url url: String, @Field("what_if_id") whatIfId: Int): Single 21 | 22 | @Headers("$HEADER_CACHEABLE: 60") 23 | @GET 24 | fun getTopWhatIfs(@Url url: String, @Query("sortby") sortby: String): Single> 25 | } 26 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/base/network/XkcdAPI.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base.network 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.Single 5 | import okhttp3.ResponseBody 6 | import retrofit2.http.* 7 | import xyz.jienan.xkcd.model.ExtraComics 8 | import xyz.jienan.xkcd.model.XkcdPic 9 | 10 | interface XkcdAPI { 11 | @get:GET("info.0.json") 12 | val latest: Observable 13 | 14 | @get:GET(XKCD_SPECIAL_LIST) 15 | val specialXkcds: Observable> 16 | 17 | @get:GET(XKCD_EXTRA_LIST) 18 | val extraComics: Observable> 19 | 20 | @Headers("$HEADER_CACHEABLE: 600") 21 | @GET("{comic_id}/info.0.json") 22 | fun getComics(@Path("comic_id") comicId: Long): Observable 23 | 24 | @Headers("$HEADER_CACHEABLE: 2419200") 25 | @GET 26 | fun getExplain(@Url url: String): Observable 27 | 28 | @GET 29 | fun getExplainWithShortCache(@Url url: String, @Header(HEADER_CACHEABLE) cache: Long): Observable 30 | 31 | @Headers("$HEADER_CACHEABLE: 600") 32 | @GET(XKCD_SEARCH_SUGGESTION) 33 | fun getXkcdsSearchResult(@Query("q") query: String): Observable> 34 | 35 | /** 36 | * Get the xkcd list with paging 37 | * 38 | * @param url 39 | * @param start the start index of xkcd list 40 | * @param reversed 0 not reversed, 1 reversed 41 | * @param size the size of returned xkcd list 42 | * @return 43 | */ 44 | @GET 45 | fun getXkcdList(@Url url: String, 46 | @Query("start") start: Int, @Query("reversed") reversed: Int, @Query("size") size: Int): Observable> 47 | 48 | @FormUrlEncoded 49 | @POST(XKCD_THUMBS_UP) 50 | fun thumbsUp(@Field("comic_id") comicId: Int): Observable 51 | 52 | @Headers("$HEADER_CACHEABLE: 60") 53 | @GET(XKCD_TOP) 54 | fun getTopXkcds(@Query("sortby") sortby: String): Observable> 55 | 56 | @Headers("$HEADER_CACHEABLE: 600") 57 | @GET 58 | fun getLocalizedXkcd(@Url url: String): Single 59 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/comics/ComicsPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.comics 2 | 3 | import androidx.fragment.app.Fragment 4 | import xyz.jienan.xkcd.comics.fragment.SingleComicFragment 5 | import xyz.jienan.xkcd.home.base.BaseStatePagerAdapter 6 | 7 | class ComicsPagerAdapter(fragment: Fragment) : BaseStatePagerAdapter(fragment) { 8 | 9 | override fun createFragment(position: Int)= SingleComicFragment.newInstance(position + 1) 10 | } 11 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/comics/SearchCursorAdapter.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.comics 2 | 3 | import android.content.Context 4 | import android.database.Cursor 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.ImageView 9 | import android.widget.TextView 10 | 11 | import androidx.cursoradapter.widget.CursorAdapter 12 | 13 | import com.bumptech.glide.Glide 14 | import com.bumptech.glide.RequestManager 15 | 16 | import xyz.jienan.xkcd.R 17 | import xyz.jienan.xkcd.base.glide.XkcdGlideUtils 18 | 19 | /** 20 | * Created by jienanzhang on 21/03/2018. 21 | */ 22 | 23 | class SearchCursorAdapter(context: Context?, c: Cursor? = null, flags: Int = 0, private val itemBgColor: Int?) : CursorAdapter(context, c, flags) { 24 | 25 | private val glide: RequestManager = Glide.with(context) 26 | 27 | override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View { 28 | val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater 29 | 30 | val view = inflater.inflate(R.layout.item_search_suggestion, parent, false) 31 | 32 | loadContent(view) 33 | 34 | return view 35 | } 36 | 37 | override fun bindView(view: View, context: Context, cursor: Cursor) { 38 | loadContent(view) 39 | } 40 | 41 | private fun loadContent(view: View) { 42 | val url = cursor.getString(1) 43 | val ivThumbnail = view.findViewById(R.id.iv_thumbnail) 44 | 45 | if (itemBgColor != null) { 46 | ivThumbnail.setBackgroundColor(itemBgColor) 47 | } 48 | 49 | XkcdGlideUtils.load(glide, url, ivThumbnail) 50 | 51 | (view.findViewById(R.id.tv_xkcd_title) as TextView).text = ivThumbnail.context.resources.getString(R.string.item_search_title, 52 | cursor.getString(3), 53 | cursor.getString(2)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/comics/contract/ComicsMainContract.java: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.comics.contract; 2 | 3 | import java.util.List; 4 | 5 | import xyz.jienan.xkcd.base.BaseView; 6 | import xyz.jienan.xkcd.home.base.ContentMainBasePresenter; 7 | import xyz.jienan.xkcd.model.XkcdPic; 8 | 9 | public interface ComicsMainContract { 10 | 11 | interface View extends BaseView { 12 | 13 | void latestXkcdLoaded(XkcdPic xkcdPic); 14 | 15 | void showFab(XkcdPic xkcdPic); 16 | 17 | void toggleFab(boolean isFavorite); 18 | 19 | void showThumbUpCount(Long thumbCount); 20 | 21 | void renderXkcdSearch(List xkcdPics); 22 | } 23 | 24 | interface Presenter extends ContentMainBasePresenter { 25 | 26 | long getBookmark(); 27 | 28 | boolean setBookmark(long index); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/comics/contract/ImageDetailPageContract.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.comics.contract 2 | 3 | import android.graphics.Bitmap 4 | 5 | import xyz.jienan.xkcd.base.BasePresenter 6 | import xyz.jienan.xkcd.base.BaseView 7 | import xyz.jienan.xkcd.model.XkcdPic 8 | 9 | interface ImageDetailPageContract { 10 | 11 | interface View : BaseView { 12 | 13 | fun setLoading(isLoading: Boolean) 14 | 15 | fun renderPic(url: String) 16 | 17 | fun renderTitle(xkcdPic: XkcdPic) 18 | 19 | fun renderSeekBar(duration: Int) 20 | 21 | fun renderFrame(bitmap: Bitmap) 22 | 23 | fun changeGifSeekBarProgress(progress: Int) 24 | 25 | fun showGifPlaySpeed(speed: Int) 26 | } 27 | 28 | interface Presenter : BasePresenter { 29 | 30 | fun requestImage(index: Int) 31 | 32 | fun parseGifData(data: ByteArray?) 33 | 34 | fun parseFrame(progress: Int) 35 | 36 | fun adjustGifSpeed(increaseByOne: Int) 37 | 38 | fun adjustGifFrame(isForward: Boolean) 39 | 40 | var isEcoMode: Boolean 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/comics/contract/SingleComicContract.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.comics.contract 2 | 3 | import android.graphics.Bitmap 4 | 5 | import xyz.jienan.xkcd.base.BasePresenter 6 | import xyz.jienan.xkcd.base.BaseView 7 | import xyz.jienan.xkcd.model.XkcdPic 8 | 9 | interface SingleComicContract { 10 | 11 | interface View : BaseView { 12 | 13 | var translationMode : Int 14 | 15 | fun explainLoaded(result: String) 16 | 17 | fun explainFailed() 18 | 19 | fun renderXkcdPic(xkcdPic: XkcdPic) 20 | 21 | fun setLoading(isLoading: Boolean) 22 | 23 | fun setAltTextVisibility(gone: Boolean) 24 | } 25 | 26 | interface Presenter : BasePresenter { 27 | 28 | fun loadXkcd(index: Int) 29 | 30 | fun getExplain(index: Long) 31 | 32 | fun updateXkcdSize(xkcdPic: XkcdPic?, resource: Bitmap?) 33 | 34 | val showLocalXkcd : Boolean 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/extra/ExtraPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.extra 2 | 3 | import androidx.fragment.app.Fragment 4 | import xyz.jienan.xkcd.extra.fragment.SingleExtraFragment 5 | import xyz.jienan.xkcd.extra.fragment.SingleExtraWebViewFragment 6 | import xyz.jienan.xkcd.home.base.BaseStatePagerAdapter 7 | import xyz.jienan.xkcd.model.ExtraComics 8 | 9 | class ExtraPagerAdapter(fragment: Fragment) : BaseStatePagerAdapter(fragment) { 10 | 11 | private var extraComicsList: List? = null 12 | 13 | override fun createFragment(position: Int): Fragment { 14 | val realPosition = position.coerceIn(0, extraComicsList!!.size - 1) 15 | val extraComics = extraComicsList!![realPosition] 16 | 17 | return if (extraComics.isWebExtra()) { 18 | SingleExtraWebViewFragment.newInstance(extraComics) 19 | } else { 20 | SingleExtraFragment.newInstance(realPosition + 1) 21 | } 22 | } 23 | 24 | fun setEntities(extraComics: List?) { 25 | extraComicsList = extraComics 26 | } 27 | 28 | override fun getItemCount(): Int = extraComicsList?.size ?: 0 29 | 30 | private fun ExtraComics.isWebExtra(): Boolean { 31 | return !links?.get(0).isNullOrBlank() 32 | } 33 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/extra/contract/ExtraMainContract.java: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.extra.contract; 2 | 3 | import java.util.List; 4 | 5 | import xyz.jienan.xkcd.base.BaseView; 6 | import xyz.jienan.xkcd.home.base.ContentMainBasePresenter; 7 | import xyz.jienan.xkcd.model.ExtraComics; 8 | 9 | public interface ExtraMainContract { 10 | 11 | interface View extends BaseView { 12 | 13 | void showExtras(List extraComics); 14 | } 15 | 16 | interface Presenter extends ContentMainBasePresenter { 17 | void observe(); 18 | void dispose(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/extra/contract/SingleExtraContract.java: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.extra.contract; 2 | 3 | import xyz.jienan.xkcd.base.BasePresenter; 4 | import xyz.jienan.xkcd.base.BaseView; 5 | import xyz.jienan.xkcd.model.ExtraComics; 6 | 7 | public interface SingleExtraContract { 8 | 9 | interface View extends BaseView { 10 | 11 | void explainLoaded(String result); 12 | 13 | void explainFailed(); 14 | 15 | void renderExtraPic(ExtraComics extraComicsInDB); 16 | 17 | void setLoading(boolean isLoading); 18 | } 19 | 20 | interface Presenter extends BasePresenter { 21 | 22 | void loadExtra(int index); 23 | 24 | void getExplain(String url, Boolean refresh); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/extra/presenter/ExtraMainPresenter.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.extra.presenter 2 | 3 | import io.objectbox.reactive.DataSubscription 4 | import io.reactivex.disposables.CompositeDisposable 5 | import timber.log.Timber 6 | import xyz.jienan.xkcd.extra.contract.ExtraMainContract 7 | import xyz.jienan.xkcd.model.ExtraComics 8 | import xyz.jienan.xkcd.model.ExtraModel 9 | import xyz.jienan.xkcd.model.persist.SharedPrefManager 10 | import java.util.* 11 | 12 | class ExtraMainPresenter(private val view: ExtraMainContract.View) : ExtraMainContract.Presenter { 13 | 14 | private val sharedPrefManager = SharedPrefManager 15 | 16 | private val compositeDisposable = CompositeDisposable() 17 | 18 | private var extraComics: List = ArrayList() 19 | 20 | private var subscription: DataSubscription? = null 21 | 22 | override fun loadLatest() { 23 | extraComics = ExtraModel.all 24 | view.showExtras(extraComics) 25 | } 26 | 27 | override fun observe() { 28 | subscription?.cancel() 29 | subscription = ExtraModel.observe 30 | .observer { 31 | if (it.size != extraComics.size && it.isNotEmpty()) { 32 | Timber.d("Show extra $it") 33 | extraComics = it 34 | view.showExtras(extraComics) 35 | subscription?.cancel() 36 | } 37 | } 38 | } 39 | 40 | override fun dispose() { 41 | subscription?.cancel() 42 | } 43 | 44 | override fun liked(index: Long) { 45 | // no-ops 46 | } 47 | 48 | override fun favorited(index: Long, isFav: Boolean) { 49 | // no-ops 50 | } 51 | 52 | override fun getInfoAndShowFab(index: Int) { 53 | // no-ops 54 | } 55 | 56 | override var latest: Int 57 | get() = extraComics.size 58 | set(value) { 59 | // no ops 60 | } 61 | 62 | override fun setLastViewed(lastViewed: Int) { 63 | sharedPrefManager.setLastViewedExtra(lastViewed) 64 | } 65 | 66 | override fun getLastViewed(latestIndex: Int) = sharedPrefManager.getLastViewedExtra(latestIndex) 67 | 68 | override fun onDestroy() { 69 | compositeDisposable.dispose() 70 | } 71 | 72 | 73 | override fun searchContent(query: String) { 74 | // no-ops 75 | } 76 | 77 | override val randomUntouchedIndex: Long 78 | get() = 0L 79 | } 80 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/extra/presenter/SingleExtraPresenter.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.extra.presenter 2 | 3 | import android.text.TextUtils 4 | import io.reactivex.android.schedulers.AndroidSchedulers 5 | import io.reactivex.disposables.Disposables 6 | import timber.log.Timber 7 | import xyz.jienan.xkcd.extra.contract.SingleExtraContract 8 | import xyz.jienan.xkcd.model.ExtraModel 9 | 10 | class SingleExtraPresenter(private val view: SingleExtraContract.View) : SingleExtraContract.Presenter { 11 | 12 | private var explainDisposable = Disposables.disposed() 13 | 14 | override fun getExplain(url: String, refresh: Boolean) { 15 | explainDisposable.dispose() 16 | ExtraModel.loadExplain(url, refresh) 17 | .observeOn(AndroidSchedulers.mainThread()) 18 | .subscribe( 19 | { explainContent -> 20 | view.explainLoaded(explainContent) 21 | if (!TextUtils.isEmpty(explainContent)) { 22 | ExtraModel.saveExtraWithExplain(url, explainContent) 23 | } 24 | }, 25 | { e -> 26 | view.explainFailed() 27 | Timber.e(e) 28 | } 29 | ).also { explainDisposable = it } 30 | } 31 | 32 | override fun loadExtra(index: Int) { 33 | view.renderExtraPic(ExtraModel.getExtra(index)) 34 | } 35 | 36 | override fun onDestroy() { 37 | explainDisposable.dispose() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/home/base/BaseStatePagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.home.base 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.viewpager2.adapter.FragmentStateAdapter 5 | 6 | 7 | abstract class BaseStatePagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { 8 | 9 | var size = 0 10 | set(value) { 11 | field = value 12 | notifyDataSetChanged() 13 | } 14 | 15 | override fun getItemCount(): Int = size 16 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/home/base/ContentMainBasePresenter.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.home.base 2 | 3 | import xyz.jienan.xkcd.base.BasePresenter 4 | 5 | interface ContentMainBasePresenter : BasePresenter { 6 | fun favorited(currentIndex: Long, isFav: Boolean) 7 | 8 | fun liked(currentIndex: Long) 9 | 10 | fun setLastViewed(lastViewed: Int) 11 | 12 | fun getInfoAndShowFab(currentIndex: Int) 13 | 14 | var latest: Int 15 | 16 | fun getLastViewed(latestIndex: Int): Int 17 | 18 | fun loadLatest() 19 | 20 | fun searchContent(query: String) 21 | 22 | val randomUntouchedIndex: Long 23 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/list/ListBaseAdapter.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.list 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | 5 | abstract class ListBaseAdapter : RecyclerView.Adapter(), IAdapter 6 | 7 | interface IAdapter { 8 | 9 | var pauseLoading: Boolean 10 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/list/activity/BaseListView.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.list.activity 2 | 3 | interface BaseListView { 4 | 5 | fun setLoading(isLoading: Boolean) 6 | 7 | fun showScroller(visibility: Int) 8 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/list/activity/WhatIfListActivity.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.list.activity 2 | 3 | import androidx.recyclerview.widget.LinearLayoutManager 4 | import androidx.recyclerview.widget.RecyclerView 5 | import xyz.jienan.xkcd.Const.FIRE_WHAT_IF_SUFFIX 6 | import xyz.jienan.xkcd.R 7 | import xyz.jienan.xkcd.list.ListBaseAdapter 8 | import xyz.jienan.xkcd.list.WhatIfListAdapter 9 | import xyz.jienan.xkcd.list.contract.WhatIfListContract 10 | import xyz.jienan.xkcd.list.presenter.WhatIfListPresenter 11 | import xyz.jienan.xkcd.model.WhatIfArticle 12 | 13 | /** 14 | * Created by jienanzhang on 22/03/2018. 15 | */ 16 | 17 | class WhatIfListActivity : BaseListActivity(), WhatIfListContract.View { 18 | 19 | override val mAdapter: ListBaseAdapter = WhatIfListAdapter() 20 | 21 | override val layoutManager = LinearLayoutManager(this) 22 | 23 | override val logSuffix = FIRE_WHAT_IF_SUFFIX 24 | 25 | override val presenter by lazy { WhatIfListPresenter(this) } 26 | 27 | override val filters = intArrayOf(R.string.filter_all_articles, R.string.filter_my_fav, R.string.filter_people_choice) 28 | 29 | override fun lastItemReached(): Boolean { 30 | if (!(mAdapter as WhatIfListAdapter).articles.isNullOrEmpty()) { 31 | val articles = mAdapter.articles!! 32 | val lastArticle = articles.last() 33 | return presenter.lastItemReached(lastArticle.num) 34 | } 35 | return false 36 | } 37 | 38 | override fun updateData(articles: List) { 39 | (mAdapter as WhatIfListAdapter).updateData(articles) 40 | } 41 | 42 | override fun getItemIndexOnPosition(position: Int) = 43 | (mAdapter as WhatIfListAdapter).getArticle(position)?.num?.toInt() 44 | } 45 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/list/contract/WhatIfListContract.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.list.contract 2 | 3 | import xyz.jienan.xkcd.base.BaseView 4 | import xyz.jienan.xkcd.list.activity.BaseListView 5 | import xyz.jienan.xkcd.list.presenter.ListPresenter 6 | import xyz.jienan.xkcd.model.WhatIfArticle 7 | 8 | interface WhatIfListContract { 9 | 10 | interface View : BaseView, BaseListView { 11 | fun updateData(articles: List) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/list/contract/XkcdListContract.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.list.contract 2 | 3 | import xyz.jienan.xkcd.base.BaseView 4 | import xyz.jienan.xkcd.list.activity.BaseListView 5 | import xyz.jienan.xkcd.list.presenter.ListPresenter 6 | import xyz.jienan.xkcd.model.XkcdPic 7 | 8 | interface XkcdListContract { 9 | 10 | interface View : BaseView, BaseListView { 11 | 12 | fun updateData(pics: List) 13 | 14 | fun isLoadingMore(isLoadingMore: Boolean) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/list/presenter/ListPresenter.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.list.presenter 2 | 3 | import xyz.jienan.xkcd.base.BasePresenter 4 | 5 | interface ListPresenter : BasePresenter { 6 | 7 | fun loadList(startIndex: Int = 1, reversed: Boolean = false) 8 | 9 | fun hasFav(): Boolean 10 | 11 | fun loadFavList() 12 | 13 | fun loadPeopleChoiceList() 14 | 15 | fun lastItemReached(index: Long): Boolean 16 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/model/ExtraComics.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import io.objectbox.annotation.Convert 5 | import io.objectbox.annotation.Entity 6 | import io.objectbox.annotation.Id 7 | import io.objectbox.converter.PropertyConverter 8 | import java.io.Serializable 9 | 10 | @Entity 11 | data class ExtraComics constructor( 12 | @Id(assignable = true) 13 | var num: Long = 0, 14 | val title: String = "", 15 | val date: String = "", 16 | val img: String = "", 17 | @SerializedName("explain") 18 | val explainUrl: String = "", 19 | var explainContent: String? = null, 20 | @Convert(converter = ListConverter::class, dbType = String::class) 21 | var links: List? = null, 22 | val alt: String = "" 23 | ) : Serializable { 24 | 25 | class ListConverter : PropertyConverter, String> { 26 | 27 | override fun convertToEntityProperty(databaseValue: String?) = 28 | databaseValue?.split("||") 29 | 30 | override fun convertToDatabaseValue(entityProperty: List) = entityProperty.joinToString("||") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/model/ExtraModel.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.model 2 | 3 | 4 | import android.text.TextUtils 5 | import io.objectbox.reactive.SubscriptionBuilder 6 | 7 | import io.reactivex.Observable 8 | import io.reactivex.android.schedulers.AndroidSchedulers 9 | import io.reactivex.schedulers.Schedulers 10 | import xyz.jienan.xkcd.base.network.NetworkService 11 | import xyz.jienan.xkcd.model.persist.BoxManager 12 | import xyz.jienan.xkcd.model.util.ExtraHtmlUtil 13 | import xyz.jienan.xkcd.model.util.XkcdExplainUtil 14 | 15 | object ExtraModel { 16 | 17 | val all: List 18 | get() = BoxManager.extraList 19 | 20 | val observe: SubscriptionBuilder> 21 | get() = BoxManager.extraListObservable 22 | 23 | fun getExtra(index: Int): ExtraComics? = BoxManager.getExtra(index) 24 | 25 | fun update(extraComics: List) { 26 | BoxManager.saveExtras(extraComics) 27 | } 28 | 29 | fun loadExplain(url: String, refresh: Boolean): Observable { 30 | val explainFromDB = BoxManager.loadExtraExplain(url) 31 | return if (!TextUtils.isEmpty(explainFromDB) && !refresh) { 32 | Observable.just(explainFromDB) 33 | } else NetworkService.xkcdAPI.getExplain(url).subscribeOn(Schedulers.io()) 34 | .map { responseBody -> XkcdExplainUtil.getExplainFromHtml(responseBody, url) } 35 | 36 | } 37 | 38 | fun saveExtraWithExplain(url: String, explainContent: String) { 39 | BoxManager.updateExtra(url, explainContent) 40 | } 41 | 42 | fun parseContentFromUrl(url: String): Observable { 43 | return ExtraHtmlUtil.getContentFromUrl(url) 44 | .observeOn(AndroidSchedulers.mainThread()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/model/Quote.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.model 2 | 3 | import android.text.TextUtils 4 | 5 | import androidx.annotation.Keep 6 | 7 | import xyz.jienan.xkcd.Const.TAG_XKCD 8 | 9 | /** 10 | * author : Stacey's dad 11 | * content : Get out while you still can 12 | * num : 61 13 | * source : xkcd 14 | */ 15 | 16 | @Keep 17 | data class Quote(var author: String = "Man in Chair", 18 | var content: String = "Sudo make me a sandwich", 19 | var num: Int = 149, 20 | var source: String = TAG_XKCD, 21 | var timestamp: Long = System.currentTimeMillis()) { 22 | 23 | override fun equals(other: Any?): Boolean { 24 | if (other is Quote) { 25 | val q = other as Quote? 26 | return (q!!.num == this.num 27 | && !TextUtils.isEmpty(q.content) 28 | && q.content == this.content) 29 | } 30 | return false 31 | } 32 | 33 | override fun hashCode() = super.hashCode() 34 | } 35 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/model/QuoteModel.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.model 2 | 3 | import android.content.res.Resources 4 | import androidx.annotation.RawRes 5 | import com.google.gson.Gson 6 | import com.google.gson.reflect.TypeToken 7 | import io.reactivex.Observable 8 | import io.reactivex.schedulers.Schedulers 9 | import xyz.jienan.xkcd.BuildConfig 10 | import xyz.jienan.xkcd.R 11 | import xyz.jienan.xkcd.base.network.NetworkService 12 | import java.util.* 13 | 14 | object QuoteModel { 15 | 16 | private val INTERVAL = (if (BuildConfig.DEBUG) 10000 else 1000 * 60 * 60 * 24).toLong() 17 | 18 | fun getQuoteOfTheDay(previousQuote: Quote, resources: Resources): Observable = 19 | Observable.just(previousQuote) 20 | .flatMap { 21 | if (System.currentTimeMillis() - it.timestamp < INTERVAL) { 22 | Observable.just(it) 23 | } else { 24 | queryNewQuote(it, resources) 25 | } 26 | } 27 | 28 | private fun queryNewQuote(quote: Quote, resources: Resources) = 29 | NetworkService.quoteAPI.quotes 30 | .subscribeOn(Schedulers.io()) 31 | .onErrorReturnItem(resources.readRawJson(R.raw.quotes)) 32 | .map { quotes -> 33 | quotes.remove(quote) 34 | quotes 35 | } 36 | .map { quotes -> quotes[Random().nextInt(quotes.size)] } 37 | .doOnNext { result -> result.timestamp = System.currentTimeMillis() } 38 | } 39 | 40 | private inline fun Resources.readRawJson(@RawRes rawResId: Int): T { 41 | openRawResource(rawResId).bufferedReader().use { 42 | return Gson().fromJson(it, object: TypeToken() {}.type) 43 | } 44 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/model/WhatIfArticle.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import io.objectbox.annotation.Entity 5 | import io.objectbox.annotation.Id 6 | 7 | @Entity 8 | data class WhatIfArticle constructor( 9 | @Id(assignable = true) 10 | var num: Long = 0, 11 | var title: String = "", 12 | var featureImg: String? = null, 13 | var content: String? = null, 14 | @Transient 15 | var date: Long = 0L, 16 | @SerializedName("date") 17 | var dateInString: String = "", 18 | var isFavorite: Boolean = false, 19 | var hasThumbed: Boolean = false, 20 | var thumbCount: Long = 0, 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/model/XkcdPic.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.model 2 | 3 | 4 | import androidx.core.text.HtmlCompat 5 | import com.google.gson.annotations.SerializedName 6 | import io.objectbox.annotation.Entity 7 | import io.objectbox.annotation.Id 8 | import io.objectbox.annotation.Uid 9 | import xyz.jienan.xkcd.model.util.XkcdSideloadUtils 10 | import java.io.Serializable 11 | 12 | /** 13 | * Created by jienanzhang on 09/07/2017. 14 | */ 15 | 16 | @Entity 17 | data class XkcdPic constructor( 18 | val year: String = "", 19 | val month: String = "", 20 | val day: String = "", 21 | @Id(assignable = true) 22 | var num: Long = 0L, 23 | @Uid(9035471003790175147L) 24 | @SerializedName("alt") 25 | val _alt: String = "", 26 | var width: Int = 0, 27 | var height: Int = 0, 28 | var isFavorite: Boolean = false, 29 | var hasThumbed: Boolean = false, 30 | @io.objectbox.annotation.Transient 31 | val thumbCount: Long = 0L, 32 | @Uid(7047913805660868881L) 33 | @SerializedName("title") 34 | val _title: String = "", 35 | var img: String = "", 36 | @io.objectbox.annotation.Transient 37 | @Transient 38 | val translated: Boolean = false) : Serializable { 39 | 40 | val targetImg: String 41 | get() = if (translated) { 42 | img 43 | } else { 44 | XkcdSideloadUtils.sideload(this).img 45 | } 46 | 47 | val title 48 | get() = HtmlCompat.fromHtml(_title, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() 49 | 50 | val alt 51 | get() = HtmlCompat.fromHtml(_alt, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() 52 | 53 | override fun equals(other: Any?) = other is XkcdPic && this.num == other.num 54 | 55 | override fun hashCode() = super.hashCode() 56 | } 57 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/model/util/ExplainLinkUtil.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.model.util 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.text.SpannableStringBuilder 7 | import android.text.method.LinkMovementMethod 8 | import android.text.style.ClickableSpan 9 | import android.text.style.URLSpan 10 | import android.view.View 11 | import android.webkit.URLUtil 12 | import android.widget.TextView 13 | import android.widget.Toast 14 | import androidx.core.text.HtmlCompat 15 | import xyz.jienan.xkcd.BuildConfig 16 | import xyz.jienan.xkcd.Const.URI_XKCD_EXPLAIN_EDIT 17 | import xyz.jienan.xkcd.R 18 | import xyz.jienan.xkcd.comics.activity.ImageDetailPageActivity 19 | import xyz.jienan.xkcd.ui.ToastUtils 20 | 21 | object ExplainLinkUtil { 22 | 23 | fun setTextViewHTML(text: TextView, html: String) { 24 | val sequence = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY) 25 | val strBuilder = SpannableStringBuilder(sequence) 26 | val urls = strBuilder.getSpans(0, sequence.length, URLSpan::class.java) 27 | urls.forEach { strBuilder.makeLinkClickable(text.context, it) } 28 | text.text = strBuilder 29 | text.movementMethod = LinkMovementMethod.getInstance() 30 | } 31 | 32 | private fun SpannableStringBuilder.makeLinkClickable(context: Context, span: URLSpan) { 33 | val start = getSpanStart(span) 34 | val end = getSpanEnd(span) 35 | val flags = getSpanFlags(span) 36 | val clickable = object : ClickableSpan() { 37 | override fun onClick(view: View) { 38 | val url = span.url 39 | if (XkcdExplainUtil.isXkcdImageLink(url)) { 40 | val id = XkcdExplainUtil.getXkcdIdFromExplainImageLink(url) 41 | ImageDetailPageActivity.startActivityFromId(context, id) 42 | } else if (URLUtil.isNetworkUrl(url)) { 43 | val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) 44 | if (browserIntent.resolveActivity(context.packageManager) != null) { 45 | context.startActivity(browserIntent) 46 | } 47 | } else if (URI_XKCD_EXPLAIN_EDIT == url) { 48 | ToastUtils.showToast(context, context.getString(R.string.uri_hint_explain_edit)) 49 | } 50 | if (BuildConfig.DEBUG) { 51 | Toast.makeText(context, url, Toast.LENGTH_SHORT).show() 52 | } 53 | } 54 | } 55 | setSpan(clickable, start, end, flags) 56 | removeSpan(span) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/model/util/ExtraHtmlUtil.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.model.util 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.schedulers.Schedulers 5 | import org.jsoup.Jsoup 6 | import xyz.jienan.xkcd.base.network.NetworkService 7 | 8 | object ExtraHtmlUtil { 9 | 10 | private const val XKCD_LINK = "http://www.xkcd.com/" 11 | 12 | fun getContentFromUrl(url: String): Observable { 13 | return NetworkService.xkcdAPI.getExplain(url) 14 | .map { responseBody -> Jsoup.parse(responseBody.string()) } 15 | .map { doc -> 16 | doc.head().appendCss("style.css") 17 | val tableElement = doc.body().selectFirst("table[width=90%]") 18 | tableElement?.removeAttr("width") 19 | val imgElements = doc.body().select("img") 20 | for (imgElement in imgElements) { 21 | val src = imgElement.attr("src") 22 | if (src != null && src.startsWith("http://imgs.xkcd")) { 23 | imgElement.attr("src", src.replaceFirst("http:".toRegex(), "https:")) 24 | } 25 | if (XKCD_LINK == imgElement.parent().attr("href")) { 26 | imgElement.parent() 27 | .removeAttr("href") 28 | .attr("href", imgElement.attr("src")) 29 | } 30 | } 31 | 32 | doc 33 | } 34 | .map { it.outerHtml() } 35 | .subscribeOn(Schedulers.io()) 36 | } 37 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/model/work/WhatIfFastLoadWorker.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.model.work 2 | 3 | import android.content.Context 4 | import androidx.work.RxWorker 5 | import androidx.work.WorkerParameters 6 | import io.reactivex.Single 7 | import timber.log.Timber 8 | import xyz.jienan.xkcd.model.WhatIfModel 9 | import xyz.jienan.xkcd.model.persist.SharedPrefManager 10 | 11 | class WhatIfFastLoadWorker(appContext: Context, workerParams: WorkerParameters) 12 | : RxWorker(appContext, workerParams) { 13 | 14 | init { 15 | Timber.d("init") 16 | } 17 | 18 | override fun createWork(): Single { 19 | return WhatIfModel.loadLatest() 20 | .doOnSubscribe { Timber.d("what if fast load start") } 21 | .map { it.num } 22 | .doOnSuccess { SharedPrefManager.latestWhatIf = it } 23 | .flatMapCompletable { WhatIfModel.fastLoadWhatIfs(it) } 24 | .toSingleDefault(Result.success()) 25 | .doOnSuccess { Timber.d("what if fast load complete") } 26 | .onErrorReturnItem(Result.failure()) 27 | } 28 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/model/work/XkcdFastLoadWorker.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.model.work 2 | 3 | import android.content.Context 4 | import androidx.work.RxWorker 5 | import androidx.work.WorkerParameters 6 | import io.reactivex.Single 7 | import timber.log.Timber 8 | import xyz.jienan.xkcd.model.XkcdModel 9 | import xyz.jienan.xkcd.model.persist.SharedPrefManager 10 | import java.util.concurrent.TimeUnit 11 | 12 | class XkcdFastLoadWorker(appContext: Context, workerParams: WorkerParameters) 13 | : RxWorker(appContext, workerParams) { 14 | 15 | init { 16 | Timber.d("init") 17 | } 18 | 19 | override fun createWork(): Single { 20 | return XkcdModel.loadLatest() 21 | .doOnSubscribe { Timber.d("xkcd fast load start") } 22 | .map { it.num } 23 | .doOnNext { Timber.d("Latest xkcd $it") } 24 | .doOnNext { SharedPrefManager.latestXkcd = it } 25 | .doOnNext { Timber.d("Ready to fast load $it") } 26 | .flatMapSingle { XkcdModel.fastLoad(it.toInt()) } 27 | .doOnNext { Timber.d("Fast load xkcd complete $it") } 28 | .map { Result.success() } 29 | .singleOrError() 30 | .doOnSuccess { Timber.d("xkcd fast load complete") } 31 | .timeout(20L, TimeUnit.SECONDS) 32 | .doOnError { Timber.e(it) } 33 | .onErrorReturnItem(Result.failure()) 34 | } 35 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/settings/ButtonPreference.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.settings 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.widget.Button 7 | import androidx.annotation.StringRes 8 | import androidx.preference.Preference 9 | import androidx.preference.PreferenceViewHolder 10 | import xyz.jienan.xkcd.R 11 | 12 | class ButtonPreference : Preference { 13 | 14 | constructor(context: Context) : super(context) 15 | 16 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) 17 | 18 | constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) 19 | 20 | @StringRes 21 | private var stringRes: Int = 0 22 | 23 | private var onClickListener: ((View) -> Unit)? = null 24 | 25 | override fun onBindViewHolder(holder: PreferenceViewHolder) { 26 | super.onBindViewHolder(holder) 27 | val button = holder.findViewById(R.id.deleteButton) 28 | if (button != null && button is Button && onClickListener != null) { 29 | if (stringRes != 0) { 30 | button.setText(stringRes) 31 | } 32 | button.setOnClickListener(onClickListener) 33 | } 34 | } 35 | 36 | fun setup(summaryText: String? = null, @StringRes resId: Int = 0, onClickListener: (View) -> Unit) { 37 | summary = summaryText 38 | this.stringRes = resId 39 | this.onClickListener = onClickListener 40 | } 41 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/settings/ManageSpaceActivity.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.settings 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import xyz.jienan.xkcd.base.BaseActivity 6 | 7 | class ManageSpaceActivity: BaseActivity() { 8 | override fun onCreate(savedInstanceState: Bundle?) { 9 | super.onCreate(savedInstanceState) 10 | supportFragmentManager.beginTransaction() 11 | .replace(android.R.id.content, ManageSpaceFragment()).commit() 12 | val actionBar = supportActionBar 13 | actionBar?.setDisplayHomeAsUpEnabled(true) 14 | } 15 | 16 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 17 | when (item.itemId) { 18 | android.R.id.home -> finish() 19 | } 20 | return false 21 | } 22 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/settings/PreferenceActivity.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.settings 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import xyz.jienan.xkcd.base.BaseActivity 6 | 7 | /** 8 | * Created by Jienan on 2018/3/9. 9 | */ 10 | 11 | class PreferenceActivity : BaseActivity() { 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | supportFragmentManager.beginTransaction() 16 | .replace(android.R.id.content, SettingsFragment()).commit() 17 | val actionBar = supportActionBar 18 | actionBar?.setDisplayHomeAsUpEnabled(true) 19 | } 20 | 21 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 22 | when (item.itemId) { 23 | android.R.id.home -> finish() 24 | } 25 | return false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/AnimUtils.java: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui; 2 | 3 | import android.graphics.drawable.Animatable; 4 | import android.graphics.drawable.Drawable; 5 | import android.os.Build; 6 | import android.widget.ImageView; 7 | 8 | public class AnimUtils { 9 | 10 | public static void vectorAnim(ImageView view, int animId, int fallbackResId) { 11 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 12 | vectorAnim(view, animId); 13 | } else { 14 | view.setImageResource(fallbackResId); 15 | } 16 | } 17 | 18 | public static void vectorAnim(ImageView view, int animId) { 19 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 20 | view.setImageResource(animId); 21 | Drawable drawable = view.getDrawable(); 22 | if (drawable instanceof Animatable) { 23 | ((Animatable) drawable).start(); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/CustomMovementMethod.java: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui; 2 | 3 | import android.text.Layout; 4 | import android.text.Selection; 5 | import android.text.Spannable; 6 | import android.text.method.LinkMovementMethod; 7 | import android.text.method.MovementMethod; 8 | import android.text.method.Touch; 9 | import android.text.style.ClickableSpan; 10 | import android.view.MotionEvent; 11 | import android.view.View; 12 | import android.widget.TextView; 13 | 14 | public class CustomMovementMethod extends LinkMovementMethod { 15 | 16 | private static CustomMovementMethod sInstance; 17 | 18 | public static MovementMethod getInstance() { 19 | if (sInstance == null) 20 | sInstance = new CustomMovementMethod(); 21 | 22 | return sInstance; 23 | } 24 | 25 | @Override 26 | public boolean canSelectArbitrarily() { 27 | return true; 28 | } 29 | 30 | @Override 31 | public void initialize(TextView widget, Spannable text) { 32 | Selection.setSelection(text, text.length()); 33 | } 34 | 35 | @Override 36 | public void onTakeFocus(TextView view, Spannable text, int dir) { 37 | if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) { 38 | if (view.getLayout() == null) { 39 | Selection.setSelection(text, text.length()); 40 | } 41 | } else { 42 | Selection.setSelection(text, text.length()); 43 | } 44 | } 45 | 46 | @Override 47 | public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { 48 | int action = event.getAction(); 49 | 50 | if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { 51 | int x = (int) event.getX(); 52 | int y = (int) event.getY(); 53 | 54 | x -= widget.getTotalPaddingLeft(); 55 | y -= widget.getTotalPaddingTop(); 56 | 57 | x += widget.getScrollX(); 58 | y += widget.getScrollY(); 59 | 60 | Layout layout = widget.getLayout(); 61 | int line = layout.getLineForVertical(y); 62 | int off = layout.getOffsetForHorizontal(line, x); 63 | 64 | ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class); 65 | 66 | if (link.length != 0) { 67 | if (action == MotionEvent.ACTION_UP) { 68 | link[0].onClick(widget); 69 | } else { 70 | Selection.setSelection(buffer, 71 | buffer.getSpanStart(link[0]), 72 | buffer.getSpanEnd(link[0])); 73 | } 74 | return true; 75 | } 76 | } 77 | return Touch.onTouchEvent(widget, buffer, event); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/Progressable.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui 2 | 3 | import android.view.animation.Animation 4 | 5 | interface Progressable { 6 | var progress: Int 7 | } 8 | 9 | interface Animatable { 10 | fun getAnimation() : Animation? 11 | 12 | fun startAnimation(animation: Animation) 13 | 14 | fun clearAnimation() 15 | 16 | fun setVisibility(visibility: Int) 17 | } 18 | 19 | interface IProgressbar : Progressable, Animatable -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/RecyclerItemClickListener.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui 2 | 3 | import android.view.GestureDetector 4 | import android.view.MotionEvent 5 | import android.view.View 6 | import androidx.recyclerview.widget.RecyclerView 7 | 8 | class RecyclerItemClickListener(private val recyclerView: RecyclerView, private val mListener: OnItemClickListener?) 9 | : RecyclerView.SimpleOnItemTouchListener() { 10 | 11 | private val mGestureDetector = GestureDetector(recyclerView.context, object : GestureDetector.SimpleOnGestureListener() { 12 | override fun onSingleTapUp(e: MotionEvent): Boolean { 13 | return true 14 | } 15 | 16 | override fun onLongPress(e: MotionEvent) { 17 | val child = recyclerView.findChildViewUnder(e.x, e.y) 18 | if (child != null && mListener != null) { 19 | mListener.onLongItemClick(child, recyclerView.getChildAdapterPosition(child)) 20 | } 21 | } 22 | }) 23 | 24 | override fun onInterceptTouchEvent(view: RecyclerView, e: MotionEvent): Boolean { 25 | val childView = view.findChildViewUnder(e.x, e.y) 26 | if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) { 27 | mListener.onItemClick(childView, view.getChildAdapterPosition(childView)) 28 | return true 29 | } 30 | return false 31 | } 32 | 33 | abstract class OnItemClickListener { 34 | 35 | open fun onItemClick(view: View, position: Int) {} 36 | 37 | open fun onLongItemClick(view: View, position: Int) {} 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/ToastUtils.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.view.Gravity 6 | import android.widget.TextView 7 | import android.widget.Toast 8 | import androidx.core.content.res.ResourcesCompat 9 | import xyz.jienan.xkcd.R 10 | 11 | object ToastUtils { 12 | 13 | private var toast: Toast? = null 14 | 15 | @SuppressLint("ShowToast") 16 | fun showToast(context: Context, text: String, position : Int = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL, duration: Int = Toast.LENGTH_SHORT) { 17 | try { 18 | toast!!.view?.isShown 19 | toast!!.setText(text) 20 | } catch (e: Exception) { 21 | toast = Toast.makeText(context.applicationContext, text, duration) 22 | } 23 | 24 | if (position != Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL) { 25 | toast!!.setGravity(position, 0, 0) 26 | } 27 | 28 | val textView = toast!!.view?.findViewById(android.R.id.message) 29 | if (textView != null) { 30 | textView.typeface = ResourcesCompat.getFont(context, R.font.xkcd) 31 | } 32 | toast!!.show() 33 | } 34 | 35 | fun cancelToast() { 36 | if (toast != null && toast!!.view?.isShown == true) { 37 | toast!!.cancel() 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/UiUtils.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui 2 | 3 | import android.content.Context 4 | import android.content.res.Configuration 5 | import android.util.TypedValue 6 | import androidx.annotation.AttrRes 7 | import androidx.annotation.ColorInt 8 | import androidx.core.content.ContextCompat 9 | 10 | @ColorInt 11 | fun Context.getColorResCompat(@AttrRes id: Int): Int { 12 | val resolvedAttr = TypedValue() 13 | theme.resolveAttribute(id, resolvedAttr, true) 14 | val colorRes = resolvedAttr.run { if (resourceId != 0) resourceId else data } 15 | return ContextCompat.getColor(this, colorRes) 16 | } 17 | 18 | fun Context.getUiNightModeFlag() = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK 19 | 20 | fun dp2px(context: Context, dpValue: Float): Int { 21 | val scale = context.resources.displayMetrics.density 22 | return (dpValue * scale + 0.5f).toInt() 23 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/like/Icon.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui.like 2 | 3 | import androidx.annotation.DrawableRes 4 | 5 | /** 6 | * Created by Joel on 23/12/2015. 7 | */ 8 | class Icon internal constructor(@DrawableRes val onIconResourceId: Int, 9 | @DrawableRes val offIconResourceId: Int, 10 | val iconType: IconType) -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/like/IconType.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui.like 2 | 3 | /** 4 | * Created by Joel on 23/12/2015. 5 | */ 6 | enum class IconType { 7 | HEART, 8 | THUMB 9 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/like/LikeButtonToggleAnimation.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui.like 2 | 3 | import android.animation.AnimatorSet 4 | import android.animation.ObjectAnimator 5 | import android.view.View 6 | import androidx.core.animation.doOnEnd 7 | import androidx.core.animation.doOnStart 8 | 9 | private val ANIMATING = "animating".hashCode() 10 | 11 | fun LikeButton.animateShow(destTranslateX: Float) { 12 | animate(true, destTranslateX) 13 | } 14 | 15 | fun LikeButton.animateHide(destTranslateX: Float) { 16 | animate(false, destTranslateX) 17 | } 18 | 19 | private fun LikeButton.animate(isShow: Boolean, destTranslateX: Float) { 20 | (getTag(ANIMATING) as AnimatorSet?)?.cancel() 21 | 22 | AnimatorSet().apply { 23 | playTogether(ObjectAnimator.ofFloat(this@animate, View.TRANSLATION_X, translationX, destTranslateX), 24 | ObjectAnimator.ofFloat(this@animate, View.ALPHA, alpha, if (isShow) 1f else 0f)) 25 | duration = 300L 26 | doOnStart { 27 | if (this@animate != null) { 28 | isClickable = isShow 29 | setTag(ANIMATING, this@apply) 30 | if (isShow) { 31 | this@animate.visibility = View.VISIBLE 32 | } 33 | } 34 | } 35 | doOnEnd { 36 | if (this@animate != null) { 37 | setTag(ANIMATING, null) 38 | if (!isShow) { 39 | this@animate.visibility = View.GONE 40 | } 41 | } 42 | } 43 | }.start() 44 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/like/LikeUtils.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui.like 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.drawable.Drawable 6 | import androidx.core.graphics.drawable.toBitmap 7 | import androidx.core.graphics.drawable.toDrawable 8 | import androidx.core.graphics.scale 9 | import xyz.jienan.xkcd.R 10 | 11 | /** 12 | * Created by Joel on 23/12/2015. 13 | */ 14 | 15 | internal val icons = listOf(Icon(R.drawable.ic_heart_on, R.drawable.ic_heart_off, IconType.HEART), 16 | Icon(R.drawable.ic_thumb_on, R.drawable.ic_thumb_off, IconType.THUMB)) 17 | 18 | internal fun Float.mapValueFromRangeToRange(fromLow: Float, fromHigh: Float, toLow: Float, toHigh: Float): Float { 19 | return toLow + (this - fromLow) / (fromHigh - fromLow) * (toHigh - toLow) 20 | } 21 | 22 | internal fun Drawable.resizeDrawable(context: Context, width: Int, height: Int): Drawable { 23 | return toBitmap(width = width, height = height, config = Bitmap.Config.ARGB_8888) 24 | .scale(width, height, true) 25 | .toDrawable(context.resources) 26 | } 27 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/like/OnLikeListener.java: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui.like; 2 | 3 | /** 4 | * Created by Joel on 23/12/2015. 5 | */ 6 | public interface OnLikeListener { 7 | void liked(LikeButton likeButton); 8 | 9 | void unliked(LikeButton likeButton); 10 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/xkcdimageview/ImageLoader.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui.xkcdimageview 2 | 3 | import android.net.Uri 4 | import androidx.annotation.UiThread 5 | import java.io.File 6 | 7 | 8 | interface ImageLoader { 9 | 10 | fun loadImage(requestId: Int, uri: Uri, callback: Callback) 11 | 12 | fun prefetch(uri: Uri) 13 | 14 | fun cancel(requestId: Int) 15 | 16 | fun cancelAll() 17 | 18 | @UiThread 19 | interface Callback { 20 | fun onCacheHit(imageType: Int, image: File) 21 | 22 | fun onCacheMiss(imageType: Int, image: File) 23 | 24 | fun onStart() 25 | 26 | fun onProgress(progress: Int) 27 | 28 | fun onFinish() 29 | 30 | fun onSuccess(image: File) 31 | 32 | fun onFail(error: Exception) 33 | } 34 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/xkcdimageview/ImageLoaderFactory.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui.xkcdimageview 2 | 3 | 4 | /** 5 | * Created by Piasy{github.com/Piasy} on 06/11/2016. 6 | * Modified by Charlie Zhang for xkcd project 7 | * 8 | * This is not a singleton, you can initialize it multiple times, but before you initialize it 9 | * again, it will use the same {@link ImageLoader} globally. 10 | */ 11 | 12 | object ImageLoaderFactory { 13 | // companion object { 14 | // @Volatile 15 | // private var sInstance: BigImageViewer? = null 16 | // 17 | // fun initialize(imageLoader: ImageLoader) { 18 | // sInstance = BigImageViewer(imageLoader) 19 | // } 20 | // 21 | // fun imageLoader(): ImageLoader { 22 | // if (sInstance == null) { 23 | // throw IllegalStateException("You must initialize BigImageViewer before use it!") 24 | // } 25 | // return sInstance!!.mImageLoader 26 | // } 27 | // 28 | // fun prefetch(vararg uris: Uri) { 29 | // if (uris == null) { 30 | // return 31 | // } 32 | // 33 | // val imageLoader = imageLoader() 34 | // for (uri in uris) { 35 | // imageLoader.prefetch(uri) 36 | // } 37 | // } 38 | // } 39 | 40 | lateinit var imageLoader: ImageLoader 41 | 42 | fun initialize(imageLoader: ImageLoader): ImageLoader { 43 | this.imageLoader = imageLoader 44 | return imageLoader 45 | } 46 | 47 | 48 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/ui/xkcdimageview/ImageViewFactory.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.ui.xkcdimageview 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView 6 | import java.io.File 7 | 8 | 9 | /** 10 | * Created by Piasy{github.com/Piasy} on 2018/8/12. 11 | */ 12 | open class ImageViewFactory { 13 | 14 | fun createMainView(context: Context, imageType: Int, imageFile: File?, 15 | initScaleType: Int): View? { 16 | return when (imageType) { 17 | ImageInfoExtractor.TYPE_GIF, ImageInfoExtractor.TYPE_ANIMATED_WEBP -> createAnimatedImageView(context, imageType, imageFile, initScaleType) 18 | ImageInfoExtractor.TYPE_STILL_WEBP, ImageInfoExtractor.TYPE_STILL_IMAGE -> createStillImageView(context) 19 | ImageInfoExtractor.TYPE_BITMAP -> createStillImageView(context) 20 | else -> createStillImageView(context) 21 | } 22 | } 23 | 24 | open fun createStillImageView(context: Context): SubsamplingScaleImageView { 25 | return SubsamplingScaleImageView(context) 26 | } 27 | 28 | open fun createAnimatedImageView(context: Context, imageType: Int, imageFile: File?, 29 | initScaleType: Int): View? { 30 | return null 31 | } 32 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/whatif/WhatIfPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.whatif 2 | 3 | import androidx.fragment.app.Fragment 4 | import xyz.jienan.xkcd.home.base.BaseStatePagerAdapter 5 | import xyz.jienan.xkcd.whatif.fragment.SingleWhatIfFragment 6 | 7 | class WhatIfPagerAdapter(fragment: Fragment) : BaseStatePagerAdapter(fragment) { 8 | 9 | override fun createFragment(position: Int) = SingleWhatIfFragment.newInstance(position + 1) 10 | } -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/whatif/contract/WhatIfMainContract.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.whatif.contract 2 | 3 | import xyz.jienan.xkcd.base.BaseView 4 | import xyz.jienan.xkcd.home.base.ContentMainBasePresenter 5 | import xyz.jienan.xkcd.model.WhatIfArticle 6 | 7 | interface WhatIfMainContract { 8 | 9 | interface View : BaseView { 10 | 11 | fun latestWhatIfLoaded(whatIfArticle: WhatIfArticle) 12 | 13 | fun showFab(whatIfArticle: WhatIfArticle) 14 | 15 | fun toggleFab(isFavorite: Boolean) 16 | 17 | fun showThumbUpCount(thumbCount: Long?) 18 | 19 | fun renderWhatIfSearch(articles: List) 20 | } 21 | 22 | interface Presenter : ContentMainBasePresenter { 23 | fun getBookmark() : Long 24 | 25 | fun setBookmark(index: Long) : Boolean 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/whatif/interfaces/ImgInterface.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.whatif.interfaces 2 | 3 | import android.webkit.JavascriptInterface 4 | 5 | class ImgInterface(private val imgCallback: ImgCallback?) { 6 | 7 | @JavascriptInterface 8 | fun doLongPress(title: String) { 9 | imgCallback?.onImgLongClick(title) 10 | } 11 | 12 | interface ImgCallback { 13 | fun onImgLongClick(title: String) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/whatif/interfaces/LatexInterface.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.whatif.interfaces 2 | 3 | import android.webkit.JavascriptInterface 4 | 5 | class LatexInterface { 6 | 7 | private var canParentScroll = true 8 | 9 | @JavascriptInterface 10 | fun onTouch(i: Int) { 11 | canParentScroll = i == 3 12 | } 13 | 14 | fun canScrollHor() = canParentScroll 15 | } 16 | -------------------------------------------------------------------------------- /xkcd/src/main/java/xyz/jienan/xkcd/whatif/interfaces/RefInterface.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.whatif.interfaces 2 | 3 | import android.webkit.JavascriptInterface 4 | 5 | class RefInterface(private val callback: RefCallback?) { 6 | 7 | @JavascriptInterface 8 | fun refContent(refs: String) { 9 | callback?.onRefClick(refs) 10 | } 11 | 12 | interface RefCallback { 13 | fun onRefClick(content: String) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /xkcd/src/main/res/anim/fadein.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /xkcd/src/main/res/anim/fadeout.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /xkcd/src/main/res/anim/fadeout_drop.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /xkcd/src/main/res/anim/rotate.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable-hdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/drawable-hdpi/ic_notification.png -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable-mdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/drawable-mdpi/ic_notification.png -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable-night/what_if_webview_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable-v21/ripple.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable-xhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/drawable-xhdpi/ic_notification.png -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable-xxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/drawable-xxhdpi/ic_notification.png -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/anim_fast_forward_shake.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/anim_fast_rewind_shake.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/anim_pause_to_play.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/anim_play_to_pause.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/graph_tile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/drawable/graph_tile.jpg -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_action_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_action_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_action_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_action_share.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_beret.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_black_hat.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_blondie.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_cueball.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_fast_forward.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_fast_rewind.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_filter_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_hairbun.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_hairy.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_heart_off.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_heart_on.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_heart_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_megan.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_pause.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_play_arrow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_ponytail.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_sort_calendar_ascending.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_sort_calendar_descending.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_thumb_off.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_thumb_on.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_what_if_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_white_circle_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_whitehat.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ic_xkcd_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/item_num_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/recycler_view_fast_scroller_bubble.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/recycler_view_fast_scroller_handle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/ripple.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /xkcd/src/main/res/drawable/what_if_webview_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /xkcd/src/main/res/font/xkcd.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 18 | -------------------------------------------------------------------------------- /xkcd/src/main/res/font/xkcd_script.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/font/xkcd_script.ttf -------------------------------------------------------------------------------- /xkcd/src/main/res/layout-land/drawer_footer.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 19 | 20 | 25 | 26 | 31 | 32 | 37 | 38 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/activity_image_webview.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 20 | 21 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/activity_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 | 17 | 18 | 27 | 28 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 23 | 24 | 28 | 29 | 30 | 31 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/dialog_explain.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 20 | 21 | 22 | 28 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/dialog_picker.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/drawer_footer.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/fragment_comic_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/fragment_extra_single.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/fragment_what_if_single.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/item_filter_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 29 | 30 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/item_search_suggestion.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 34 | 35 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/item_what_if_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 20 | 32 | 33 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/item_xkcd_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 20 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/likeview.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 18 | 19 | 27 | 28 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/nav_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 23 | 24 | 33 | 34 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/pref_widget_delete.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /xkcd/src/main/res/layout/rv_scroller.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 18 | 19 | 27 | 28 | -------------------------------------------------------------------------------- /xkcd/src/main/res/menu/menu_drawer.xml: -------------------------------------------------------------------------------- 1 | 2 |

3 | 7 | 11 | 15 | 19 | -------------------------------------------------------------------------------- /xkcd/src/main/res/menu/menu_extra.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /xkcd/src/main/res/menu/menu_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 17 | 18 | 23 | 24 | -------------------------------------------------------------------------------- /xkcd/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 16 | 21 | 26 | 27 | 32 | -------------------------------------------------------------------------------- /xkcd/src/main/res/menu/menu_what_if.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 15 | -------------------------------------------------------------------------------- /xkcd/src/main/res/menu/menu_xkcd.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | 16 | 17 | 23 | 24 | 29 | -------------------------------------------------------------------------------- /xkcd/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /xkcd/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /xkcd/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /xkcd/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /xkcd/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /xkcd/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /xkcd/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /xkcd/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /xkcd/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /xkcd/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /xkcd/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /xkcd/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjn0505/xkcd-Android/d088aee32a4995128aae69a9354ab3285bd668da/xkcd/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /xkcd/src/main/res/raw/quotes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "author": "Stacey's dad", 4 | "content": "Get out while you still can", 5 | "num": 61, 6 | "source": "xkcd" 7 | }, 8 | { 9 | "author": "Man in Chair", 10 | "content": "Sudo make me a sandwich", 11 | "num": 149, 12 | "source": "xkcd" 13 | }, 14 | { 15 | "author": "Cueball", 16 | "content": "Someone is wrong on the Internet", 17 | "num": 386, 18 | "source": "xkcd" 19 | }, 20 | { 21 | "author": "Man in Chair", 22 | "content": "Ouch", 23 | "num": 744, 24 | "source": "xkcd" 25 | }, 26 | { 27 | "author": "xkcd", 28 | "content": "BACKWARD. I READ THINK ENGINEERS HIGHWAY", 29 | "num": 781, 30 | "source": "xkcd" 31 | }, 32 | { 33 | "author": "Title Text", 34 | "content": "Our brains have just one scale, and we resize our experiences to fit", 35 | "num": 915, 36 | "source": "xkcd" 37 | }, 38 | { 39 | "author": "xkcd", 40 | "content": "I disagree strongly with whatever work this quote is attached to", 41 | "num": 1942, 42 | "source": "xkcd" 43 | }, 44 | { 45 | "author": "xkcd", 46 | "content": "This quote was taken out of context", 47 | "num": 1942, 48 | "source": "xkcd" 49 | }, 50 | { 51 | "author": "Randall", 52 | "content": "But at least now we know", 53 | "num": 8, 54 | "source": "what if" 55 | }, 56 | { 57 | "author": "Tim Minchin", 58 | "content": "I really think that I would have somebody else", 59 | "num": 9, 60 | "source": "what if" 61 | }, 62 | { 63 | "author": "Randall", 64 | "content": "Unit cancellation is weird", 65 | "num": 11, 66 | "source": "what if" 67 | }, 68 | { 69 | "author": "Black Hat", 70 | "content": "What if we tried more power", 71 | "num": 13, 72 | "source": "what if" 73 | }, 74 | { 75 | 76 | "author": "Randall", 77 | "content": "All that changes when this cat enters the equation", 78 | "num": 15, 79 | "source": "what if" 80 | 81 | }, 82 | { 83 | "author": "Randall", 84 | "content": "This can't really happen", 85 | "num": 39, 86 | "source": "what if" 87 | }, 88 | { 89 | "author": "Randall", 90 | "content": "Pressure cookers are dangerous", 91 | "num": 40, 92 | "source": "what if" 93 | } 94 | ] 95 | -------------------------------------------------------------------------------- /xkcd/src/main/res/raw/xkcd_extra.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "num": 1, 4 | "date": "2006.10.11", 5 | "title": "Blue Eyes", 6 | "img": "https://www.explainxkcd.com/wiki/images/a/a0/Blue_Eyes.jpg", 7 | "links": ["https://xkcd.com/blue_eyes.html", "https://xkcd.com/solution.html"], 8 | "explain": "https://www.explainxkcd.com/wiki/index.php/Blue_Eyes" 9 | }, 10 | { 11 | "num": 2, 12 | "date": "2009.06.17", 13 | "title": "Conservation", 14 | "img": "https://www.explainxkcd.com/wiki/images/1/10/conservation.png", 15 | "explain": "https://www.explainxkcd.com/wiki/index.php/Conservation" 16 | }, 17 | { 18 | "num": 3, 19 | "date": "2009.08.11", 20 | "title": "Prescriptions", 21 | "img": "https://www.explainxkcd.com/wiki/images/b/bc/prescriptions.png", 22 | "explain": "https://www.explainxkcd.com/wiki/index.php/Prescriptions" 23 | }, 24 | { 25 | "num": 4, 26 | "date": "2011.03.19", 27 | "title": "Radiation", 28 | "img": "https://imgs.xkcd.com/blag/radiation.png", 29 | "explain": "https://www.explainxkcd.com/wiki/index.php/Radiation" 30 | }, 31 | { 32 | "num": 5, 33 | "date": "2011.08.19", 34 | "title": "Five-Minute Comics: Part 4", 35 | "img": "https://www.explainxkcd.com/wiki/images/6/60/five_minute_comics_part_4.png", 36 | "explain": "https://www.explainxkcd.com/wiki/index.php/Five-Minute_Comics:_Part_4" 37 | }, 38 | { 39 | "num": 6, 40 | "date": "2013.10.04", 41 | "title": "The Rise of Open Access", 42 | "img": "https://www.explainxkcd.com/wiki/images/archive/4/48/20150825153049%21the_rise_of_open_access.jpg", 43 | "explain": "https://www.explainxkcd.com/wiki/index.php/The_Rise_of_Open_Access" 44 | }, 45 | { 46 | "num": 7, 47 | "date": "2015.10.22", 48 | "title": "XKCD Marks the Spot", 49 | "img": "https://www.explainxkcd.com/wiki/images/9/9c/world_polio_day.png", 50 | "explain": "https://www.explainxkcd.com/wiki/index.php/XKCD_Marks_the_Spot" 51 | }, 52 | { 53 | "num": 8, 54 | "date": "2019.08.04", 55 | "title": "Disappearing Sunday Update", 56 | "img": "https://www.explainxkcd.com/wiki/images/6/6c/disappearing_sunday_update.png", 57 | "explain": "https://www.explainxkcd.com/wiki/index.php/Disappearing_Sunday_Update" 58 | }, 59 | { 60 | "num": 9, 61 | "date": "2020.07.06", 62 | "title": "No One Was Hurt", 63 | "img": "https://explainxkcd.com/wiki/images/7/7f/no_one_was_hurt.png", 64 | "explain": "https://explainxkcd.com/wiki/index.php/No_One_Was_Hurt", 65 | "alt": "See how the smoke obscures things so you don't see the approaching tornado until the firework streamers start to swirl right before the lightning hits the launcher. Thank God Cousin Ed was filming in slow motion. And, uh, that no one was hurt." 66 | } 67 | ] 68 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values-b+zh+hant+TW/api.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://xkcd.jienan.xyz/%d/info.0.json?locale=zh-tw 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values-b+zh/api.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://xkcd.jienan.xyz/%d/info.0.json 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values-de/api.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://xkcd.jienan.xyz/%d/info.0.json?locale=de 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Teile dieses xkcd 3 | Bisherige 4 | Nächster 5 | Suche 6 | 7 | Übersetzung by @xkcDE 8 | Eine Schaltfläche wird angezeigt, wenn eine Übersetzung vorhanden ist 9 | Übersetzungen ignorieren 10 | Übersetzung 11 | Original 12 | 13 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values-es/api.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://xkcd.jienan.xyz/%d/info.0.json?locale=es 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Traducción by @es.xkcd.com 3 | Se mostrará un cambio cuando exista traducción 4 | Ignorar traducciones 5 | Traducción 6 | Original 7 | 8 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values-fr/api.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://xkcd.jienan.xyz/%d/info.0.json?locale=fr 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Traduction by @lapin.org 3 | Un commutateur sera affiché lorsque la traduction existe 4 | Ignorer les traductions 5 | Traduction 6 | Original 7 | 8 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1F2020 4 | #121212 5 | #111111 6 | #AA1F2020 7 | 8 | #ADD8E6 9 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values-ru/api.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://xkcd.jienan.xyz/%d/info.0.json?locale=ru 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Перевод by @xkcd.ru 3 | Переключатель будет показан, когда перевод существует 4 | Игнорировать переводы 5 | Перевод 6 | Oригинал 7 | 8 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values/api.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #283593 6 | #AA3F51B5 7 | 8 | #FF4081 9 | 10 | #aeaeae 11 | #e91e63 12 | #FFF 13 | #2196f3 14 | #DEffffff 15 | 16 | #ffffff 17 | 18 | #42000000 19 | #50DDDDDD 20 | 21 | 22 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10dp 4 | -------------------------------------------------------------------------------- /xkcd/src/main/res/values/non_translatable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | xkcd 4 | 5 | xkcd 6 | what if 7 | extra 8 | 9 | 10 | M25.1 2.6c12.5 0 18.7 5.9 24.9 17.6C56.2 8.5 62.4 2.6 74.9 2.6c13.8 0 25 10.5 25 23.6C100 50 75 73.7 50 97.4 25 73.7 0 50 0 26.2c0-13 11.3-23.7 25.1-23.6z 11 | 12 | 13 | 14 | M1 21h4V9H1v12zm22-11a2 2 0 0 0-2-2h-6.3l1-4.6v-0.3c0-0.4-0.2-0.8-0.5-1l-1-1.1-6.6 6.6A2 2 0 0 0 7 9v10c0 1.1 0.9 2 2 2h9a2 2 0 0 0 1.8-1.2l3-7 0.2-0.8v-2 15 | 16 | 17 | 1 18 | 10 19 | 30 20 | arrow_1 21 | arrow_10 22 | arrow_30 23 | 24 | what if 25 | xkcd 26 | 27 | %d-%s 28 | %s - %s 29 | 30 | %s.%s.%s 31 | 32 | %d - %s 33 | - %s %s:%d 34 | -------------------------------------------------------------------------------- /xkcd/src/main/res/xml/backup_descriptor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /xkcd/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | xkcd.com 5 | 6 | -------------------------------------------------------------------------------- /xkcd/src/main/res/xml/searchable.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /xkcd/src/proprietary/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /xkcd/src/proprietary/java/xyz/jienan/xkcd/FlavorUtils.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd 2 | 3 | import android.app.Application 4 | import com.google.android.gms.common.ConnectionResult 5 | import com.google.android.gms.common.GoogleApiAvailabilityLight 6 | import com.google.android.gms.tasks.OnCompleteListener 7 | import com.google.firebase.crashlytics.FirebaseCrashlytics 8 | import com.google.firebase.installations.FirebaseInstallations 9 | import com.google.firebase.messaging.FirebaseMessaging 10 | import timber.log.Timber 11 | import java.util.* 12 | import kotlin.concurrent.thread 13 | 14 | object FlavorUtils { 15 | 16 | private const val FCM_TOPIC_NEW_COMICS = "new_comics" 17 | 18 | private const val FCM_TOPIC_NEW_WHAT_IF = "new_what_if" 19 | 20 | fun init() { 21 | FirebaseMessaging.getInstance().apply { 22 | subscribeToTopic(FCM_TOPIC_NEW_COMICS) 23 | subscribeToTopic(FCM_TOPIC_NEW_WHAT_IF) 24 | } 25 | if (BuildConfig.DEBUG) { 26 | thread(start = true) { 27 | Timber.d("FCM id ${FirebaseInstallations.getInstance().id}") 28 | } 29 | 30 | FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task -> 31 | if (!task.isSuccessful) { 32 | Timber.w(task.exception, "Fetching FCM registration token failed") 33 | return@OnCompleteListener 34 | } 35 | 36 | // Get new FCM registration token 37 | val token = task.result 38 | Timber.d("FCM token $token") 39 | }) 40 | } 41 | } 42 | 43 | fun updateLocale() { 44 | if (!BuildConfig.DEBUG) { 45 | FirebaseCrashlytics.getInstance() 46 | .setCustomKey("locale", Locale.getDefault().toString()) 47 | } 48 | } 49 | 50 | fun getGmsAvailability(app: Application): Boolean { 51 | val status = GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(app) 52 | Timber.d("GMS status = $status") 53 | return status == ConnectionResult.SUCCESS 54 | } 55 | } -------------------------------------------------------------------------------- /xkcd/src/proprietary/java/xyz/jienan/xkcd/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base 2 | 3 | import android.content.SharedPreferences 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.preference.PreferenceManager 7 | import com.google.firebase.analytics.FirebaseAnalytics 8 | import xyz.jienan.xkcd.Const.FIRE_UX_ACTION 9 | import xyz.jienan.xkcd.Const.PREF_FONT 10 | import xyz.jienan.xkcd.R 11 | import xyz.jienan.xkcd.comics.activity.ImageDetailPageActivity 12 | import xyz.jienan.xkcd.home.MainActivity 13 | 14 | /** 15 | * Created by Jienan on 2018/3/9. 16 | */ 17 | 18 | abstract class BaseActivity : AppCompatActivity() { 19 | 20 | private val mFirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(this) } 21 | 22 | protected val sharedPreferences: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) } 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | setTheme() 26 | super.onCreate(savedInstanceState) 27 | } 28 | 29 | protected fun logUXEvent(event: String, bundle: Bundle? = null) { 30 | mFirebaseAnalytics.logEvent(FIRE_UX_ACTION, (bundle ?: Bundle()).apply { 31 | putString(FIRE_UX_ACTION, event) 32 | }) 33 | } 34 | 35 | private fun setTheme() { 36 | val fontPref = sharedPreferences.getBoolean(PREF_FONT, false) 37 | if (fontPref) { 38 | when (this) { 39 | is MainActivity -> setTheme(R.style.CustomActionBarTheme) 40 | is ImageDetailPageActivity -> setTheme(R.style.TransparentBackgroundTheme) 41 | else -> setTheme(R.style.AppBarTheme) 42 | } 43 | } else { 44 | when (this) { 45 | is MainActivity -> setTheme(R.style.CustomActionBarFontTheme) 46 | is ImageDetailPageActivity -> setTheme(R.style.TransparentBackgroundTheme) 47 | else -> setTheme(R.style.AppBarFontTheme) 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /xkcd/src/proprietary/java/xyz/jienan/xkcd/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.base 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.annotation.LayoutRes 8 | import androidx.fragment.app.Fragment 9 | import com.google.firebase.analytics.FirebaseAnalytics 10 | import xyz.jienan.xkcd.Const.FIRE_UX_ACTION 11 | 12 | abstract class BaseFragment : Fragment() { 13 | 14 | private val mFirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(requireContext()) } 15 | 16 | @get:LayoutRes 17 | protected abstract val layoutResId: Int 18 | 19 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = 20 | inflater.inflate(layoutResId, container, false) 21 | 22 | protected fun logUXEvent(event: String, params: Map? = null) { 23 | val bundle = Bundle() 24 | bundle.putString(FIRE_UX_ACTION, event) 25 | if (!params.isNullOrEmpty()) { 26 | for (key in params.keys) { 27 | val value = params[key] 28 | if (value!!.matches("-?\\d+".toRegex())) { 29 | bundle.putInt(key, Integer.valueOf(value)) 30 | } else { 31 | bundle.putString(key, params[key]) 32 | } 33 | } 34 | } 35 | mFirebaseAnalytics.logEvent(FIRE_UX_ACTION, bundle) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /xkcd/src/release/java/xyz/jienan/xkcd/DebugUtils.java: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd; 2 | 3 | import android.content.Context; 4 | 5 | import timber.log.Timber; 6 | 7 | public class DebugUtils { 8 | static boolean init() { 9 | if (BuildConfig.DEBUG) { 10 | Timber.plant(new Timber.DebugTree()); 11 | } 12 | return true; 13 | } 14 | 15 | static void debugDB(Context context) { 16 | // no-ops 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /xkcd/src/release/res/raw/keep.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /xkcd/src/release/res/values/debug_placeholder.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /xkcd/src/test/java/xyz/jienan/xkcd/comics/activity/ImageWebViewActivityTest.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.comics.activity 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | class ImageWebViewActivityTest { 7 | 8 | private val url1663 = "https://zjn0505.github.io/xkcd-undressed/1663/#d23aed20-d830-11e9-8008-42010a8e0003" 9 | 10 | @Test 11 | fun check1663UrlParse() { 12 | val uuid = ImageWebViewActivity().extractUuidFrom1663url(url1663) 13 | assertEquals("d23aed20-d830-11e9-8008-42010a8e0003", uuid) 14 | } 15 | } -------------------------------------------------------------------------------- /xkcd/src/test/java/xyz/jienan/xkcd/model/util/WhatIfArticleUtilTest.kt: -------------------------------------------------------------------------------- 1 | package xyz.jienan.xkcd.model.util 2 | 3 | import okhttp3.ResponseBody 4 | import org.junit.Test 5 | 6 | class WhatIfArticleUtilTest { 7 | 8 | @Test(expected = NullPointerException::class) 9 | fun `get articles from null response body`() { 10 | WhatIfArticleUtil.getArticlesFromArchive(ResponseBody.create(null, "")) 11 | } 12 | } --------------------------------------------------------------------------------