├── .gitignore ├── .gitmodules ├── .travis.yml ├── .travis ├── build.sh └── secrets.tar.enc ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── top │ │ └── rechinx │ │ └── meow │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── sites │ │ │ ├── dmzj.xml │ │ │ ├── kuaikan.xml │ │ │ ├── picacg.xml │ │ │ └── shuhui.xml │ ├── java │ │ └── top │ │ │ └── rechinx │ │ │ └── meow │ │ │ ├── App.kt │ │ │ ├── core │ │ │ ├── extension │ │ │ │ ├── ExtensionInstallReceiver.kt │ │ │ │ ├── ExtensionLoader.kt │ │ │ │ ├── ExtensionManager.kt │ │ │ │ └── model │ │ │ │ │ ├── Extension.kt │ │ │ │ │ ├── InstallStep.kt │ │ │ │ │ └── LoadResult.kt │ │ │ ├── network │ │ │ │ ├── NetworkHelper.kt │ │ │ │ ├── OkHttpExtensions.kt │ │ │ │ ├── ProgressListener.kt │ │ │ │ └── ProgressResponseBody.kt │ │ │ └── source │ │ │ │ ├── Configurable.kt │ │ │ │ ├── CouldLogin.kt │ │ │ │ ├── HttpSource.kt │ │ │ │ ├── HttpSourceFetcher.kt │ │ │ │ ├── Source.kt │ │ │ │ ├── SourceManager.kt │ │ │ │ ├── StubSource.kt │ │ │ │ ├── internal │ │ │ │ └── Dmzj.kt │ │ │ │ └── model │ │ │ │ ├── Filter.kt │ │ │ │ ├── FilterList.kt │ │ │ │ ├── MangaPage.kt │ │ │ │ ├── PagedList.kt │ │ │ │ ├── SChapter.kt │ │ │ │ ├── SChapterImpl.kt │ │ │ │ ├── SManga.kt │ │ │ │ └── SMangaImpl.kt │ │ │ ├── data │ │ │ ├── cache │ │ │ │ ├── ChapterCache.kt │ │ │ │ └── CoverCache.kt │ │ │ ├── database │ │ │ │ ├── AppDatabse.kt │ │ │ │ ├── dao │ │ │ │ │ ├── ChapterDao.kt │ │ │ │ │ ├── MangaDao.kt │ │ │ │ │ └── TaskDao.kt │ │ │ │ └── model │ │ │ │ │ ├── Chapter.kt │ │ │ │ │ ├── Manga.kt │ │ │ │ │ └── Task.kt │ │ │ ├── download │ │ │ │ ├── DownloadProvider.kt │ │ │ │ └── DownloadService.kt │ │ │ ├── preference │ │ │ │ └── PreferenceHelper.kt │ │ │ └── repository │ │ │ │ ├── CataloguePager.kt │ │ │ │ ├── ChapterPager.kt │ │ │ │ ├── ChapterRepository.kt │ │ │ │ ├── MangaRepository.kt │ │ │ │ └── Pager.kt │ │ │ ├── di │ │ │ ├── AppComponent.kt │ │ │ ├── AppModule.kt │ │ │ └── DatabaseModule.kt │ │ │ ├── exception │ │ │ └── NoMoreResultException.kt │ │ │ ├── glide │ │ │ ├── FileFetcher.kt │ │ │ ├── GlideModule.kt │ │ │ ├── LibraryMangaUrlFetcher.kt │ │ │ ├── MangaModelLoader.kt │ │ │ ├── MangaSignature.kt │ │ │ └── PassthroughModelLoader.kt │ │ │ ├── global │ │ │ ├── Constants.kt │ │ │ └── Extras.kt │ │ │ ├── ui │ │ │ ├── about │ │ │ │ └── AboutFragment.kt │ │ │ ├── base │ │ │ │ ├── BaseActivity.kt │ │ │ │ ├── BaseAdapter.kt │ │ │ │ ├── BaseFragment.kt │ │ │ │ ├── BaseMvpActivity.kt │ │ │ │ └── BaseMvpActivityWithoutReflection.kt │ │ │ ├── details │ │ │ │ ├── DetailActivity.kt │ │ │ │ ├── DetailAdapter.kt │ │ │ │ ├── DetailPresenter.kt │ │ │ │ ├── chapters │ │ │ │ │ ├── ChaptersActivity.kt │ │ │ │ │ └── ChaptersAdapter.kt │ │ │ │ └── items │ │ │ │ │ ├── ChapterItem.kt │ │ │ │ │ ├── HeaderItem.kt │ │ │ │ │ └── LoadItem.kt │ │ │ ├── extension │ │ │ │ ├── ExtensionActivity.kt │ │ │ │ ├── ExtensionAdapter.kt │ │ │ │ └── ExtensionPresenter.kt │ │ │ ├── filter │ │ │ │ ├── FilterActivity.kt │ │ │ │ ├── FilterPresenter.kt │ │ │ │ └── items │ │ │ │ │ ├── CatalogueItem.kt │ │ │ │ │ ├── CheckboxItem.kt │ │ │ │ │ ├── GroupItem.kt │ │ │ │ │ ├── HeaderItem.kt │ │ │ │ │ ├── ProgressItem.kt │ │ │ │ │ ├── SectionItems.kt │ │ │ │ │ ├── SelectItem.kt │ │ │ │ │ ├── SeparatorItem.kt │ │ │ │ │ ├── SortGroup.kt │ │ │ │ │ ├── SortItem.kt │ │ │ │ │ ├── TextItem.kt │ │ │ │ │ └── TriStateItem.kt │ │ │ ├── grid │ │ │ │ ├── GridAdapter.kt │ │ │ │ ├── download │ │ │ │ │ ├── DownloadFragment.kt │ │ │ │ │ └── DownloadPresenter.kt │ │ │ │ ├── favorite │ │ │ │ │ ├── FavoriteFragment.kt │ │ │ │ │ └── FavoritePresenter.kt │ │ │ │ ├── history │ │ │ │ │ ├── HistoryFragment.kt │ │ │ │ │ └── HistoryPresenter.kt │ │ │ │ └── items │ │ │ │ │ └── GridItem.kt │ │ │ ├── home │ │ │ │ ├── HomeFragment.kt │ │ │ │ └── MainActivity.kt │ │ │ ├── reader │ │ │ │ ├── ReaderActivity.kt │ │ │ │ ├── ReaderPresenter.kt │ │ │ │ ├── ReaderProgressBar.kt │ │ │ │ ├── ReaderSetting.kt │ │ │ │ ├── loader │ │ │ │ │ ├── ChapterLoader.kt │ │ │ │ │ ├── DownloadedPageLoader.kt │ │ │ │ │ ├── HttpPageLoader.kt │ │ │ │ │ └── PageLoader.kt │ │ │ │ ├── model │ │ │ │ │ ├── ChapterTransition.kt │ │ │ │ │ ├── ReaderChapter.kt │ │ │ │ │ ├── ReaderPage.kt │ │ │ │ │ └── ViewerChapters.kt │ │ │ │ └── viewer │ │ │ │ │ ├── BaseViewer.kt │ │ │ │ │ ├── GestureDetectorWithLongTap.kt │ │ │ │ │ ├── pager │ │ │ │ │ ├── Pager.kt │ │ │ │ │ ├── PagerButton.kt │ │ │ │ │ ├── PagerConfig.kt │ │ │ │ │ ├── PagerPageHolder.kt │ │ │ │ │ ├── PagerTransitionHolder.kt │ │ │ │ │ ├── PagerViewer.kt │ │ │ │ │ ├── PagerViewerAdapter.kt │ │ │ │ │ ├── PagerViewers.kt │ │ │ │ │ └── ViewPagerAdapter.kt │ │ │ │ │ └── webtoon │ │ │ │ │ ├── WebtoonAdapter.kt │ │ │ │ │ ├── WebtoonBaseHolder.kt │ │ │ │ │ ├── WebtoonConfig.kt │ │ │ │ │ ├── WebtoonFrame.kt │ │ │ │ │ ├── WebtoonLayoutManager.kt │ │ │ │ │ ├── WebtoonPageHolder.kt │ │ │ │ │ ├── WebtoonRecyclerView.kt │ │ │ │ │ ├── WebtoonTransitionHolder.kt │ │ │ │ │ └── WebtoonViewer.kt │ │ │ ├── result │ │ │ │ ├── ResultActivity.kt │ │ │ │ ├── ResultAdapter.kt │ │ │ │ └── ResultPresenter.kt │ │ │ ├── setting │ │ │ │ ├── MainSettingsFragment.kt │ │ │ │ ├── SettingsActivity.kt │ │ │ │ ├── ThemeActivity.kt │ │ │ │ └── ThemeAdapter.kt │ │ │ ├── source │ │ │ │ ├── SourceAdapter.kt │ │ │ │ └── SourceFragment.kt │ │ │ └── task │ │ │ │ ├── TaskActivity.kt │ │ │ │ ├── TaskAdapter.kt │ │ │ │ └── TaskPresenter.kt │ │ │ ├── utils │ │ │ ├── DiskUtil.kt │ │ │ ├── FileUtils.kt │ │ │ ├── Hash.kt │ │ │ ├── ImageUtil.kt │ │ │ ├── RetryWithDelay.kt │ │ │ ├── ThemeHelper.kt │ │ │ ├── UniFileUtils.java │ │ │ └── Utility.kt │ │ │ └── widget │ │ │ ├── AutofitRecyclerView.kt │ │ │ ├── ChapterButton.kt │ │ │ ├── CheckableButton.kt │ │ │ ├── MaterialChapterButton.kt │ │ │ ├── MaterialSideSheet.kt │ │ │ ├── MiniClockText.java │ │ │ ├── ReverseSeekBar.java │ │ │ ├── ScrollAwareFABBehavior.java │ │ │ ├── SimpleTextWatcher.kt │ │ │ ├── TagGroup.java │ │ │ └── TintBottomNavigationView.java │ └── res │ │ ├── anim │ │ ├── enter_from_bottom.xml │ │ ├── enter_from_top.xml │ │ ├── exit_to_bottom.xml │ │ ├── exit_to_top.xml │ │ └── fade_in_long.xml │ │ ├── color │ │ ├── bottom_navigation_colors.xml │ │ ├── selector_switch_thumb.xml │ │ └── selector_switch_track.xml │ │ ├── drawable-hdpi │ │ ├── card_background.9.png │ │ ├── ic_av_pause_grey_24dp_img.png │ │ ├── ic_av_play_arrow_grey_img.png │ │ ├── ic_clear_grey_24dp_img.png │ │ ├── ic_continue_read_white_24dp.png │ │ ├── ic_done_white_24dp.png │ │ └── ic_start_white_24dp.png │ │ ├── drawable-xhdpi │ │ ├── ic_av_pause_grey_24dp_img.png │ │ ├── ic_av_play_arrow_grey_img.png │ │ ├── ic_clear_grey_24dp_img.png │ │ └── ic_done_white_24dp.png │ │ ├── drawable │ │ ├── bottom_navigation_colors.xml │ │ ├── btn_status_selector.xml │ │ ├── empty_drawable_32dp.xml │ │ ├── gradient_shape.xml │ │ ├── ic_about_black_24dp.xml │ │ ├── ic_arrow_down_black_32dp.xml │ │ ├── ic_arrow_down_white_32dp.xml │ │ ├── ic_arrow_up_black_32dp.xml │ │ ├── ic_arrow_up_white_32dp.xml │ │ ├── ic_check_box_24dp.xml │ │ ├── ic_check_box_outline_blank_24dp.xml │ │ ├── ic_check_box_x_24dp.xml │ │ ├── ic_chevron_right_black_24dp.xml │ │ ├── ic_chevron_right_white_24dp.xml │ │ ├── ic_delete_white_24dp.xml │ │ ├── ic_done_all_white_24dp.xml │ │ ├── ic_expand_more_black_24dp.xml │ │ ├── ic_expand_more_white_24dp.xml │ │ ├── ic_extension_white_24dp.xml │ │ ├── ic_favorite_border_white_24dp.xml │ │ ├── ic_favorite_white_24dp.xml │ │ ├── ic_file_download_white_24dp.xml │ │ ├── ic_filter_white_24dp.xml │ │ ├── ic_import_contacts_black_24dp.xml │ │ ├── ic_launch_white_24dp.xml │ │ ├── ic_palette_white_24dp.xml │ │ ├── ic_pause_white_24dp.xml │ │ ├── ic_play_arrow_white_24dp.xml │ │ ├── ic_search_white_24dp.xml │ │ ├── ic_settings_white_24dp.xml │ │ ├── ic_skip_next_white_24dp.xml │ │ ├── ic_skip_previous_white_24dp.xml │ │ ├── ic_widgets_black_24dp.xml │ │ ├── item_selector_light.xml │ │ └── list_item_selector_light.xml │ │ ├── layout │ │ ├── activity_chapters_selection.xml │ │ ├── activity_detail.xml │ │ ├── activity_extension.xml │ │ ├── activity_filter.xml │ │ ├── activity_home.xml │ │ ├── activity_reader.xml │ │ ├── activity_result.xml │ │ ├── activity_settings.xml │ │ ├── activity_task.xml │ │ ├── activity_theme.xml │ │ ├── custom_dialog_preivew.xml │ │ ├── custom_drawer_header.xml │ │ ├── custom_filter_sidesheet.xml │ │ ├── custom_progress_bar.xml │ │ ├── custom_reader_info.xml │ │ ├── custom_setting_sheet.xml │ │ ├── custom_toolbar.xml │ │ ├── fragment_about.xml │ │ ├── fragment_download_queue.xml │ │ ├── fragment_grid.xml │ │ ├── fragment_home.xml │ │ ├── fragment_source.xml │ │ ├── item_chapter.xml │ │ ├── item_chapter_loadmore.xml │ │ ├── item_detail_header.xml │ │ ├── item_grid_fit.xml │ │ ├── item_navigation_checkbox.xml │ │ ├── item_navigation_checkdtext.xml │ │ ├── item_navigation_group.xml │ │ ├── item_navigation_spinner.xml │ │ ├── item_navigation_text.xml │ │ ├── item_progress.xml │ │ ├── item_result.xml │ │ ├── item_source.xml │ │ ├── item_spinner_common.xml │ │ ├── item_task.xml │ │ └── item_theme.xml │ │ ├── menu │ │ ├── bar_menu.xml │ │ ├── filter_menu.xml │ │ ├── menu_about.xml │ │ ├── menu_bottom_nav.xml │ │ ├── menu_detail.xml │ │ ├── menu_grid.xml │ │ ├── menu_task.xml │ │ └── reader.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── colors.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values │ │ ├── array.xml │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── pref_settings.xml │ └── test │ └── java │ └── top │ └── rechinx │ └── meow │ ├── ExampleUnitTest.kt │ └── test.xml ├── art ├── 01.jpg ├── 02.jpg ├── 03.jpg ├── 04.jpg ├── 05.jpg └── 06.jpg ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore └── DirectionalViewPager │ ├── .gitignore │ ├── build.gradle │ └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── androidx │ │ └── core │ │ └── view │ │ ├── DirectionalViewPager.java │ │ └── PagerAdapter.java │ └── res │ └── values │ └── attrs.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | /app/build 10 | /.travis/keystore.properties 11 | /.travis/meow.keystore 12 | /app/release 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "library/Rikka"] 2 | path = library/Rikka 3 | url = https://github.com/ReChinX/Rikka.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | android: 3 | components: 4 | - tools 5 | - platform-tools 6 | - build-tools-28.0.3 7 | - android-28 8 | - android-27 9 | - extra-android-m2repository 10 | - extra-android-support 11 | - extra-google-m2repository 12 | - extra-android-m2repository 13 | - extra-android-m2repository 14 | - extra-android-support 15 | licenses: 16 | - android-sdk-preview-license-.+ 17 | - android-sdk-license-.+ 18 | - google-gdk-license-.+ 19 | before_install: 20 | - openssl aes-256-cbc -K $encrypted_ce6de0293363_key -iv $encrypted_ce6de0293363_iv 21 | -in .travis/secrets.tar.enc -out .travis/secrets.tar -d 22 | - tar xvf .travis/secrets.tar 23 | - chmod +x gradlew 24 | script: 25 | - "chmod +x .travis/build.sh" 26 | - ".travis/build.sh" 27 | before_cache: 28 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 29 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 30 | cache: 31 | directories: 32 | - "$HOME/.gradle/caches/" 33 | - "$HOME/.gradle/wrapper/" 34 | deploy: 35 | provider: releases 36 | api_key: "${GITHUB_TOKEN}" 37 | file: meow-v*.apk 38 | file_glob: true 39 | skip_cleanup: true 40 | on: 41 | tags: true 42 | -------------------------------------------------------------------------------- /.travis/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$TRAVIS_TAG" ]; then 4 | ./gradlew clean assembleDebug 5 | 6 | COMMIT_COUNT=$(git rev-list --count HEAD) 7 | export ARTIFACT="meow-r${COMMIT_COUNT}.apk" 8 | 9 | mv app/build/outputs/apk/debug/app-debug.apk $ARTIFACT 10 | else 11 | ./gradlew clean assembleRelease 12 | 13 | export ARTIFACT="meow-${TRAVIS_TAG}.apk" 14 | 15 | mv app/build/outputs/apk/release/app-release.apk $ARTIFACT 16 | fi -------------------------------------------------------------------------------- /.travis/secrets.tar.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/.travis/secrets.tar.enc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meow for Android 2 | 3 | [![Build Status](https://www.travis-ci.org/ReChinX/Meow.svg?branch=master)](https://travis-ci.org/ReChinX/Meow) 4 | 5 | Another comic reader client for Android. 6 | 7 | You can download release version from here: 8 | 9 | ## Screenshots 10 | 11 | 12 | 13 | 14 | 15 | ## Introduction 16 | 17 | - Page reading 18 | 19 | - Scorll reading 20 | 21 | - Auto update 22 | 23 | - Login support 24 | 25 | - Plugin-like comic source 26 | 27 | ## Thanks 28 | 29 | - [Rxjava2](https://github.com/ReactiveX/RxJava) 30 | - [ButterKnife](https://github.com/JakeWharton/butterknife) 31 | - [Okhttp](https://github.com/square/okhttp) 32 | - [Glide](https://github.com/bumptech/glide) 33 | - [DiscreteSeekBar](https://github.com/AnderWeb/discreteSeekBar) 34 | - [PhotoView](https://github.com/chrisbanes/PhotoView) 35 | - [SmartRefreshLayout](https://github.com/scwang90/SmartRefreshLayout) 36 | - [MaterialSearchView](https://github.com/MiguelCatalan/MaterialSearchView) 37 | - [J2V8](https://github.com/eclipsesource/J2V8) 38 | - [RxPermissions](https://github.com/tbruyelle/RxPermissions) 39 | - [CheckVersionLib](https://github.com/AlexLiuSheng/CheckVersionLib) 40 | - [SiteD](https://github.com/noear/SiteD) 41 | - [Cimoc](https://github.com/Arachnid-27/Cimoc) 42 | 43 | ## License 44 | 45 | ``` 46 | GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 47 | 48 | Copyright (C) 2018 Chin 49 | 50 | This program comes with ABSOLUTELY NO WARRANTY. 51 | This is free software, and you are welcome to redistribute it under certain conditions. 52 | ``` 53 | 54 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original sourceId file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/top/rechinx/meow/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow 2 | 3 | import androidx.test.InstrumentationRegistry 4 | import androidx.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("top.rechinx.meow", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/extension/model/Extension.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.extension.model 2 | 3 | import top.rechinx.meow.core.source.Source 4 | 5 | data class Extension(val name: String, 6 | val pkgName: String, 7 | val versionName: String, 8 | val versionCode: Int, 9 | val sources: List, 10 | val hasUpdate: Boolean = false) -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/extension/model/InstallStep.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.extension.model 2 | 3 | enum class InstallStep { 4 | Pending, Downloading, Installing, Installed, Error; 5 | 6 | fun isCompleted(): Boolean { 7 | return this == Installed || this == Error 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/extension/model/LoadResult.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.extension.model 2 | 3 | sealed class LoadResult { 4 | 5 | class Success(val extension: Extension) : LoadResult() 6 | class Error(val message: String? = null) : LoadResult() { 7 | constructor(exception: Throwable) : this(exception.message) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/network/NetworkHelper.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.network 2 | 3 | import android.content.Context 4 | import com.facebook.stetho.okhttp3.StethoInterceptor 5 | import okhttp3.OkHttpClient 6 | 7 | class NetworkHelper(context: Context) { 8 | 9 | val client = OkHttpClient.Builder().addNetworkInterceptor(StethoInterceptor()).build() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/network/OkHttpExtensions.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.network 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.ObservableEmitter 5 | import io.reactivex.ObservableOnSubscribe 6 | import okhttp3.Call 7 | import okhttp3.OkHttpClient 8 | import okhttp3.Request 9 | import okhttp3.Response 10 | import org.reactivestreams.Subscription 11 | import java.util.concurrent.atomic.AtomicBoolean 12 | 13 | fun Call.asObservable(): Observable { 14 | return Observable.create { emitter -> 15 | try { 16 | val call = clone() 17 | val response = call.execute() 18 | if(!emitter.isDisposed) { 19 | emitter.onNext(response) 20 | emitter.onComplete() 21 | } 22 | }catch (e: Exception) { 23 | if(!emitter.isDisposed) { 24 | emitter.onError(e) 25 | } 26 | } 27 | } 28 | } 29 | 30 | 31 | fun Call.asObservableSuccess(): Observable { 32 | return asObservable().doOnNext { response -> 33 | if (!response.isSuccessful) { 34 | response.close() 35 | throw Exception("HTTP error ${response.code()}") 36 | } 37 | } 38 | } 39 | 40 | fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { 41 | val progressClient = newBuilder() 42 | .cache(null) 43 | .addNetworkInterceptor { chain -> 44 | val originalResponse = chain.proceed(chain.request()) 45 | originalResponse.newBuilder() 46 | .body(ProgressResponseBody(originalResponse.body()!!, listener)) 47 | .build() 48 | } 49 | .build() 50 | 51 | return progressClient.newCall(request) 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/network/ProgressListener.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.network 2 | 3 | interface ProgressListener { 4 | fun update(bytesRead: Long, contentLength: Long, done: Boolean) 5 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/network/ProgressResponseBody.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.network 2 | 3 | import okhttp3.MediaType 4 | import okhttp3.ResponseBody 5 | import okio.* 6 | import java.io.IOException 7 | 8 | class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() { 9 | 10 | private val bufferedSource: BufferedSource by lazy { 11 | Okio.buffer(source(responseBody.source())) 12 | } 13 | 14 | override fun contentType(): MediaType { 15 | return responseBody.contentType()!! 16 | } 17 | 18 | override fun contentLength(): Long { 19 | return responseBody.contentLength() 20 | } 21 | 22 | override fun source(): BufferedSource { 23 | return bufferedSource 24 | } 25 | 26 | private fun source(source: Source): Source { 27 | return object : ForwardingSource(source) { 28 | internal var totalBytesRead = 0L 29 | 30 | @Throws(IOException::class) 31 | override fun read(sink: Buffer, byteCount: Long): Long { 32 | val bytesRead = super.read(sink, byteCount) 33 | // read() returns the number of bytes read, or -1 if this sourceId is exhausted. 34 | totalBytesRead += if (bytesRead != -1L) bytesRead else 0 35 | progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L) 36 | return bytesRead 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/Configurable.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source 2 | 3 | import android.preference.PreferenceScreen 4 | 5 | interface Configurable { 6 | 7 | fun setupPreferenceScreen(screen: PreferenceScreen) 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/CouldLogin.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source 2 | 3 | import io.reactivex.Observable 4 | import okhttp3.Response 5 | 6 | interface CouldLogin { 7 | 8 | fun isLogged(): Boolean 9 | 10 | fun login(username: String, password: String): Observable 11 | 12 | fun isAuthenticationSuccessful(response: Response): Boolean 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/HttpSourceFetcher.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source 2 | 3 | import io.reactivex.Observable 4 | import top.rechinx.meow.core.source.model.MangaPage 5 | 6 | fun HttpSource.getImageUrl(page: MangaPage): Observable { 7 | page.status = MangaPage.LOAD_PAGE 8 | return fetchImageUrl(page) 9 | .doOnError { page.status = MangaPage.ERROR } 10 | .onErrorReturn { null } 11 | .doOnNext { page.imageUrl = it } 12 | .map { page } 13 | } 14 | 15 | fun HttpSource.fetchAllImageUrlsFromPageList(pages: List): Observable { 16 | return Observable.fromIterable(pages) 17 | .filter { !it.imageUrl.isNullOrEmpty() } 18 | .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) 19 | } 20 | 21 | fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List): Observable { 22 | return Observable.fromIterable(pages) 23 | .filter { it.imageUrl.isNullOrEmpty() } 24 | .concatMap { getImageUrl(it) } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/Source.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source 2 | 3 | import io.reactivex.Observable 4 | import top.rechinx.meow.core.source.model.* 5 | 6 | interface Source { 7 | 8 | val id: Long 9 | 10 | val name: String 11 | 12 | fun fetchPopularManga(page: Int): Observable> 13 | 14 | fun fetchSearchManga(query: String, page: Int, filters: FilterList): Observable> 15 | 16 | fun fetchMangaInfo(url: String): Observable 17 | 18 | fun fetchChapters(page: Int, url: String): Observable> 19 | 20 | fun fetchMangaPages(chapter: SChapter): Observable> 21 | 22 | fun getFilterList(): FilterList 23 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/SourceManager.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source 2 | 3 | import android.content.Context 4 | import top.rechinx.meow.core.source.internal.Dmzj 5 | 6 | class SourceManager(private val context: Context) { 7 | 8 | private val sourcesMap = mutableMapOf() 9 | 10 | private val stubSourcesMap = mutableMapOf() 11 | 12 | init { 13 | createInternalSources().forEach{ registerSource(it)} 14 | } 15 | 16 | open fun get(sourceKey: Long): Source? { 17 | return sourcesMap[sourceKey] 18 | } 19 | 20 | open fun getOrStub(sourceKey: Long): Source { 21 | return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) { 22 | StubSource(sourceKey) 23 | } 24 | } 25 | 26 | fun getSources() = sourcesMap.values 27 | 28 | fun registerSource(source: Source, overwrite: Boolean = false) { 29 | if(overwrite || !sourcesMap.containsKey(source.id)) { 30 | sourcesMap[source.id] = source 31 | } 32 | } 33 | 34 | fun unregisterSource(source: Source) { 35 | sourcesMap.remove(source.id) 36 | } 37 | 38 | private fun createInternalSources(): List = listOf( 39 | Dmzj() 40 | //Shuhui(), 41 | //EHentai() 42 | ) 43 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/StubSource.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source 2 | 3 | import io.reactivex.Observable 4 | import top.rechinx.meow.core.source.model.* 5 | import java.lang.Exception 6 | 7 | class StubSource(override val id: Long): Source { 8 | 9 | override val name: String = id.toString() 10 | 11 | override fun fetchPopularManga(page: Int): Observable> { 12 | return Observable.error(Exception()) 13 | } 14 | 15 | override fun fetchSearchManga(query: String, page: Int, filters: FilterList): Observable> { 16 | return Observable.error(Exception()) 17 | } 18 | 19 | override fun fetchMangaInfo(cid: String): Observable { 20 | return Observable.error(Exception()) 21 | } 22 | 23 | override fun fetchChapters(page: Int, cid: String): Observable> { 24 | return Observable.error(Exception()) 25 | } 26 | 27 | override fun fetchMangaPages(chapter: SChapter): Observable> { 28 | return Observable.error(Exception()) 29 | } 30 | 31 | override fun getFilterList(): FilterList { 32 | return FilterList() 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/model/Filter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source.model 2 | 3 | sealed class Filter(val name: String, var state: T) { 4 | open class Header(name: String) : Filter(name, 0) 5 | open class Separator(name: String = "") : Filter(name, 0) 6 | abstract class Select(name: String, val values: Array, state: Int = 0) : Filter(name, state) 7 | abstract class Text(name: String, state: String = "") : Filter(name, state) 8 | abstract class CheckBox(name: String, state: Boolean = false) : Filter(name, state) 9 | abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter(name, state) { 10 | fun isIgnored() = state == STATE_IGNORE 11 | fun isIncluded() = state == STATE_INCLUDE 12 | fun isExcluded() = state == STATE_EXCLUDE 13 | 14 | companion object { 15 | const val STATE_IGNORE = 0 16 | const val STATE_INCLUDE = 1 17 | const val STATE_EXCLUDE = 2 18 | } 19 | } 20 | abstract class Group(name: String, state: List): Filter>(name, state) 21 | 22 | abstract class Sort(name: String, val values: Array, state: Selection? = null) 23 | : Filter(name, state) { 24 | data class Selection(val index: Int, val ascending: Boolean) 25 | } 26 | 27 | override fun equals(other: Any?): Boolean { 28 | if (this === other) return true 29 | if (other !is Filter<*>) return false 30 | 31 | return name == other.name && state == other.state 32 | } 33 | 34 | override fun hashCode(): Int { 35 | var result = name.hashCode() 36 | result = 31 * result + (state?.hashCode() ?: 0) 37 | return result 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/model/FilterList.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source.model 2 | 3 | data class FilterList(val list: List>) : List> by list { 4 | 5 | constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList()) 6 | 7 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/model/MangaPage.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source.model 2 | 3 | import io.reactivex.processors.FlowableProcessor 4 | import io.reactivex.processors.PublishProcessor 5 | import io.reactivex.subjects.Subject 6 | import top.rechinx.meow.core.network.ProgressListener 7 | 8 | open class MangaPage( 9 | val index: Int, 10 | val url: String, 11 | var imageUrl: String? 12 | ) : ProgressListener { 13 | 14 | val number: Int 15 | get() = index + 1 16 | 17 | @Transient @Volatile var status: Int = 0 18 | set(value) { 19 | field = value 20 | statusProcessor?.onNext(value) 21 | } 22 | @Transient @Volatile var progress: Int = 0 23 | 24 | 25 | @Transient private var statusProcessor: FlowableProcessor? = null 26 | 27 | override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { 28 | progress = if (contentLength > 0) { 29 | (100 * bytesRead / contentLength).toInt() 30 | } else { 31 | -1 32 | } 33 | } 34 | 35 | fun setStatusProcessor(processor: FlowableProcessor?) { 36 | this.statusProcessor = processor 37 | } 38 | 39 | companion object { 40 | 41 | const val QUEUE = 0 42 | const val LOAD_PAGE = 1 43 | const val DOWNLOAD_IMAGE = 2 44 | const val READY = 3 45 | const val ERROR = 4 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/model/PagedList.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source.model 2 | 3 | data class PagedList(val list: List, val hasNextPage: Boolean) -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/model/SChapter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source.model 2 | 3 | interface SChapter { 4 | 5 | var url: String? 6 | 7 | var name: String? 8 | 9 | var date_updated: Long 10 | 11 | var chapter_number: String? 12 | 13 | fun copyFrom(other: SChapter) { 14 | if(other.url != null) { 15 | this.url = other.url 16 | } 17 | 18 | if(other.name != null) { 19 | this.name = other.name 20 | } 21 | 22 | this.date_updated = other.date_updated 23 | } 24 | 25 | companion object { 26 | fun create(): SChapter { 27 | return SChapterImpl() 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/model/SChapterImpl.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source.model 2 | 3 | class SChapterImpl: SChapter { 4 | 5 | override var url: String? = null 6 | 7 | override var name: String? = null 8 | 9 | override var date_updated: Long = 0 10 | 11 | override var chapter_number: String? = null 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/model/SManga.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source.model 2 | 3 | abstract class SManga { 4 | 5 | abstract var url: String? 6 | 7 | abstract var title: String? 8 | 9 | abstract var author: String? 10 | 11 | abstract var description: String? 12 | 13 | abstract var genre: String? 14 | 15 | abstract var status: Int 16 | 17 | abstract var thumbnail_url: String? 18 | 19 | abstract var initialized: Boolean 20 | 21 | fun copyFrom(other: SManga) { 22 | if (other.url != null) 23 | url = other.url 24 | 25 | if (other.author != null) 26 | author = other.author 27 | 28 | if (other.author != null) 29 | author = other.author 30 | 31 | if (other.description != null) 32 | description = other.description 33 | 34 | if (other.genre != null) 35 | genre = other.genre 36 | 37 | if (other.thumbnail_url != null) 38 | thumbnail_url = other.thumbnail_url 39 | 40 | if (other.title != null) 41 | title = other.title 42 | 43 | status = other.status 44 | 45 | if (!initialized) 46 | initialized = other.initialized 47 | } 48 | 49 | companion object { 50 | const val UNKNOWN = 0 51 | const val ONGOING = 1 52 | const val COMPLETED = 2 53 | 54 | fun create(): SManga { 55 | return SMangaImpl() 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/core/source/model/SMangaImpl.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.core.source.model 2 | 3 | class SMangaImpl: SManga() { 4 | 5 | override var url: String? = null 6 | 7 | override var title: String? = null 8 | 9 | override var author: String? = null 10 | 11 | override var description: String? = null 12 | 13 | override var genre: String? = null 14 | 15 | override var status: Int = 0 16 | 17 | override var thumbnail_url: String? = null 18 | 19 | override var initialized: Boolean = false 20 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/data/cache/CoverCache.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.data.cache 2 | 3 | import android.content.Context 4 | import top.rechinx.meow.utils.DiskUtil 5 | import java.io.File 6 | 7 | class CoverCache(val context: Context) { 8 | 9 | private val cacheDir = context.getExternalFilesDir("covers") ?: 10 | File(context.filesDir, "covers").also { it.mkdirs() } 11 | 12 | fun getCoverFile(thumbnailUrl: String): File { 13 | return File(cacheDir, DiskUtil.hashKeyForDisk(thumbnailUrl)) 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/data/database/AppDatabse.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.data.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import top.rechinx.meow.data.database.dao.ChapterDao 6 | import top.rechinx.meow.data.database.dao.MangaDao 7 | import top.rechinx.meow.data.database.dao.TaskDao 8 | import top.rechinx.meow.data.database.model.Chapter 9 | import top.rechinx.meow.data.database.model.Manga 10 | import top.rechinx.meow.data.database.model.Task 11 | 12 | @Database(entities = [Manga::class, Chapter::class, Task::class], version = 1) 13 | abstract class AppDatabase: RoomDatabase() { 14 | 15 | abstract fun mangaDao(): MangaDao 16 | 17 | abstract fun chapterDao(): ChapterDao 18 | 19 | abstract fun taskDao(): TaskDao 20 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/data/database/dao/ChapterDao.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.data.database.dao 2 | 3 | import androidx.room.* 4 | import io.reactivex.Maybe 5 | import io.reactivex.Observable 6 | import top.rechinx.meow.data.database.model.Chapter 7 | import top.rechinx.meow.data.database.model.Manga 8 | 9 | @Dao 10 | interface ChapterDao { 11 | 12 | @Insert(onConflict = OnConflictStrategy.REPLACE) 13 | fun insertChapters(chapters: List) 14 | 15 | @Insert 16 | fun insertChapter(chapter: Chapter): Long 17 | 18 | @Query("SELECT * FROM Chapter WHERE manga_id = :mangaId") 19 | fun getChapters(mangaId: Long) : Maybe> 20 | 21 | @Query("SELECT * FROM Chapter WHERE url = :chapterUrl AND manga_id = :mangaId") 22 | fun getChapter(chapterUrl: String, mangaId: Long) : Chapter? 23 | 24 | @Query("SELECT * FROM Chapter WHERE id = :chapterId") 25 | fun getChapter(chapterId: Long): Chapter? 26 | 27 | @Delete 28 | fun deleteChapters(list: List) 29 | 30 | @Update 31 | fun updateChapter(chapter: Chapter) 32 | 33 | @Query("UPDATE Chapter SET download = 0, complete = 0 WHERE manga_id = :mangaId") 34 | fun updateChapterDownloadInfoByMangaId(mangaId: Long) 35 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/data/database/dao/MangaDao.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.data.database.dao 2 | 3 | import androidx.room.* 4 | import io.reactivex.Flowable 5 | import io.reactivex.Maybe 6 | import io.reactivex.Observable 7 | import top.rechinx.meow.data.database.model.Manga 8 | 9 | @Dao 10 | interface MangaDao { 11 | 12 | @Insert(onConflict = OnConflictStrategy.REPLACE) 13 | fun insertManga(manga: Manga) 14 | 15 | @Update 16 | fun updateManga(manga: Manga) 17 | 18 | @Query("SELECT * FROM Manga WHERE sourceId = :sourceId AND url = :url") 19 | fun loadManga(sourceId: Long, url: String): Manga? 20 | 21 | @Query("SELECT * FROM Manga WHERE id = :mangaId") 22 | fun load(mangaId: Long): Manga? 23 | 24 | @Query("SELECT * FROM Manga WHERE id = :mangaId") 25 | fun loadManga(mangaId: Long): Maybe 26 | 27 | @Query("SELECT * FROM Manga") 28 | fun listMangas(): Flowable> 29 | 30 | @Query("SELECT * FROM Manga WHERE favorite = 1") 31 | fun listFavorite(): Flowable> 32 | 33 | @Query("SELECT * FROM Manga WHERE history = 1") 34 | fun listHistory(): Flowable> 35 | 36 | @Query("SELECT * FROM Manga WHERE download = 1") 37 | fun listDownload(): Flowable> 38 | 39 | @Query("SELECT * FROM Manga WHERE sourceId = :sourceId AND url = :url") 40 | fun relayManga(sourceId: Long, url: String): Flowable 41 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/data/database/dao/TaskDao.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.data.database.dao 2 | 3 | import androidx.room.* 4 | import io.reactivex.Flowable 5 | import io.reactivex.Maybe 6 | import io.reactivex.Observable 7 | import top.rechinx.meow.data.database.model.Task 8 | 9 | @Dao 10 | interface TaskDao { 11 | 12 | @Insert 13 | fun insert(task: Task) : Long 14 | 15 | @Update 16 | fun update(task: Task) 17 | 18 | @Query("SELECT * FROM Task WHERE mangaId = :mangaId") 19 | fun list(mangaId: Long) : List 20 | 21 | @Query("SELECT * FROM Task WHERE mangaId = :mangaId") 22 | fun listInRx(mangaId: Long) : Maybe> 23 | 24 | @Query("DELETE FROM Task WHERE mangaId = :mangaId") 25 | fun deleteByMangaId(mangaId: Long) 26 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/data/database/model/Chapter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.data.database.model 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | import androidx.room.ColumnInfo 6 | import androidx.room.Entity 7 | import androidx.room.Ignore 8 | import androidx.room.PrimaryKey 9 | import top.rechinx.meow.core.source.model.SChapter 10 | 11 | @Entity 12 | data class Chapter(@ColumnInfo override var url: String?, 13 | @ColumnInfo override var name: String?, 14 | @ColumnInfo override var date_updated: Long, 15 | @ColumnInfo override var chapter_number: String?, 16 | @PrimaryKey(autoGenerate = true) var id: Long = 0, 17 | @ColumnInfo var manga_id: Long = 0, 18 | @ColumnInfo var last_page_read: Int = 0, 19 | @ColumnInfo var download: Boolean = false, 20 | @ColumnInfo var complete: Boolean = false) : SChapter, Parcelable { 21 | constructor() : this(null, null, 0, null) 22 | 23 | @Ignore constructor(source: Parcel) : this( 24 | source.readString(), 25 | source.readString(), 26 | source.readLong(), 27 | source.readString(), 28 | source.readLong(), 29 | source.readLong(), 30 | source.readInt(), 31 | 1 == source.readInt(), 32 | 1 == source.readInt() 33 | ) 34 | 35 | override fun describeContents() = 0 36 | 37 | override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { 38 | writeString(url) 39 | writeString(name) 40 | writeLong(date_updated) 41 | writeString(chapter_number) 42 | writeLong(id) 43 | writeLong(manga_id) 44 | writeInt(last_page_read) 45 | writeInt(if(download) 1 else 0) 46 | writeInt(if(complete) 1 else 0) 47 | } 48 | 49 | companion object { 50 | fun create(): Chapter = Chapter() 51 | 52 | @JvmField 53 | val CREATOR: Parcelable.Creator = object : Parcelable.Creator { 54 | override fun createFromParcel(source: Parcel): Chapter = Chapter(source) 55 | override fun newArray(size: Int): Array = arrayOfNulls(size) 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/data/database/model/Manga.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.data.database.model 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.Ignore 6 | import androidx.room.PrimaryKey 7 | import top.rechinx.meow.core.source.model.SManga 8 | 9 | @Entity 10 | data class Manga(@ColumnInfo override var url: String?, 11 | @ColumnInfo override var title: String?, 12 | @ColumnInfo override var author: String?, 13 | @ColumnInfo override var description: String?, 14 | @ColumnInfo override var genre: String?, 15 | @ColumnInfo override var status: Int, 16 | @ColumnInfo override var thumbnail_url: String?, 17 | @ColumnInfo override var initialized: Boolean, 18 | @ColumnInfo var sourceName: String?, 19 | @ColumnInfo var last_chapter: String?, 20 | @PrimaryKey(autoGenerate = true) var id: Long = 0, 21 | @ColumnInfo var favorite: Boolean = false, 22 | @ColumnInfo var last_read_chapter_id: Long = -1, 23 | @ColumnInfo var sourceId: Long = 0, 24 | @ColumnInfo var last_update: Long = 0, 25 | @ColumnInfo var viewer: Int = 0, 26 | @ColumnInfo var history: Boolean = false, 27 | @ColumnInfo var download: Boolean = false): SManga() { 28 | 29 | constructor(): this(null, null, null, null, null, 0, null, false, null, null) 30 | 31 | @Ignore constructor(source: Long):this() { 32 | this.sourceId = source 33 | } 34 | 35 | @Ignore constructor(pathUrl: String, title: String, source: Long = 0): this() { 36 | this.sourceId = source 37 | this.title = title 38 | this.url = pathUrl 39 | } 40 | 41 | companion object { 42 | fun create(pathUrl: String, title: String, sourceId: Long = 0): Manga = Manga().apply { 43 | url = pathUrl 44 | this.title = title 45 | this.sourceId = sourceId 46 | } 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/data/preference/PreferenceHelper.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.data.preference 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.os.Environment 6 | import com.f2prateek.rx.preferences2.Preference 7 | import com.f2prateek.rx.preferences2.RxSharedPreferences 8 | import top.rechinx.meow.global.Constants 9 | import java.io.File 10 | 11 | fun Preference.getOrDefault(): T = get() ?: defaultValue()!! 12 | 13 | class PreferenceHelper(context: Context) { 14 | 15 | private val prefs = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) 16 | private val rxPrefs = RxSharedPreferences.create(prefs) 17 | 18 | private val defaultDownloadsDir = Uri.fromFile( 19 | File(Environment.getExternalStorageDirectory().absolutePath + File.separator + 20 | "Meow", "downloads")) 21 | 22 | fun fullscreen() = rxPrefs.getBoolean(Constants.PREF_FULL_SCREEN, true) 23 | 24 | fun pageTransitions() = rxPrefs.getBoolean(Constants.PREF_ENABLE_TRANSITIONS, true) 25 | 26 | fun readerMode() = prefs.getInt(Constants.PREF_READER_MODE, 1) 27 | 28 | fun hiddenReaderInfo() = rxPrefs.getBoolean(Constants.PREF_HIDE_READER_INFO, false) 29 | 30 | fun sourceSwitch(sourceId: Long) = rxPrefs.getBoolean("source_switch_$sourceId", false) 31 | 32 | fun setSourceSwitch(sourceId: Long, switcher: Boolean) { 33 | prefs.edit().putBoolean("source_switch_$sourceId", switcher) 34 | .apply() 35 | } 36 | 37 | fun downloadOnlyOverWifi() = prefs.getBoolean(Constants.PREF_DOWNLOAD_ONLY_WIFI, true) 38 | 39 | fun downloadsDirectory() = rxPrefs.getString(Constants.PREF_DOWNLOAD_DIRECTORY, defaultDownloadsDir.toString()) 40 | 41 | fun enableVolumeKeys() = rxPrefs.getBoolean(Constants.PREF_ENABLE_VOLUME_KEYS, false) 42 | 43 | fun readWithVolumeKeysInverted() = rxPrefs.getBoolean(Constants.PREF_READ_WITH_VOLUME_KEYS_INVERTED, false) 44 | 45 | companion object { 46 | 47 | const val PREFERENCES_NAME = "meow_preferences" 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/data/repository/CataloguePager.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.data.repository 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.android.schedulers.AndroidSchedulers 5 | import io.reactivex.schedulers.Schedulers 6 | import top.rechinx.meow.core.source.Source 7 | import top.rechinx.meow.core.source.model.SManga 8 | import top.rechinx.meow.core.source.model.FilterList 9 | import top.rechinx.meow.core.source.model.PagedList 10 | import top.rechinx.meow.exception.NoMoreResultException 11 | 12 | class CataloguePager(val source: Source, val query: String, val filters: FilterList): Pager() { 13 | 14 | override fun requestNext() : Observable> { 15 | val observable = if(query.isBlank() && filters.isEmpty()) { 16 | source.fetchPopularManga(currentPage) 17 | } else { 18 | source.fetchSearchManga(query, currentPage, filters) 19 | } 20 | 21 | return observable.subscribeOn(Schedulers.io()) 22 | .observeOn(AndroidSchedulers.mainThread()) 23 | .doOnNext { 24 | if(it.list.isEmpty()) { 25 | throw NoMoreResultException() 26 | } else { 27 | onPageReceived(it) 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/data/repository/ChapterPager.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.data.repository 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.android.schedulers.AndroidSchedulers 5 | import io.reactivex.schedulers.Schedulers 6 | import top.rechinx.meow.core.source.Source 7 | import top.rechinx.meow.core.source.model.SChapter 8 | import top.rechinx.meow.core.source.model.PagedList 9 | import top.rechinx.meow.exception.NoMoreResultException 10 | 11 | class ChapterPager(val source: Source, val cid: String): Pager() { 12 | 13 | override fun requestNext(): Observable> { 14 | val page = currentPage 15 | 16 | return source.fetchChapters(page, cid) 17 | .subscribeOn(Schedulers.io()) 18 | .observeOn(AndroidSchedulers.mainThread()) 19 | .doOnNext { 20 | if(it.list.isNotEmpty()) { 21 | onPageReceived(it) 22 | } else { 23 | throw NoMoreResultException() 24 | } 25 | } 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/data/repository/ChapterRepository.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.data.repository 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Observable 5 | import io.reactivex.Scheduler 6 | import io.reactivex.android.schedulers.AndroidSchedulers 7 | import io.reactivex.schedulers.Schedulers 8 | import top.rechinx.meow.core.source.SourceManager 9 | import top.rechinx.meow.data.database.dao.ChapterDao 10 | import top.rechinx.meow.data.database.model.Chapter 11 | import top.rechinx.meow.data.database.model.Manga 12 | 13 | class ChapterRepository(val sourceManager: SourceManager, 14 | val chapterDao: ChapterDao) { 15 | 16 | fun getLocalChapters(manga: Manga) : List { 17 | return chapterDao.getChapters(manga.id).blockingGet() 18 | } 19 | 20 | fun updateChapter(chapter: Chapter) { 21 | Completable.fromAction { 22 | chapterDao.updateChapter(chapter) 23 | }.subscribeOn(Schedulers.io()) 24 | .observeOn(AndroidSchedulers.mainThread()) 25 | .subscribe() 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/data/repository/Pager.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.data.repository 2 | 3 | import com.jakewharton.rxrelay2.PublishRelay 4 | import io.reactivex.Observable 5 | import top.rechinx.meow.core.source.model.PagedList 6 | 7 | abstract class Pager(var currentPage: Int = 1) { 8 | 9 | var hasNextPage = true 10 | private set 11 | 12 | val results = PublishRelay.create>>() 13 | 14 | abstract fun requestNext(): Observable> 15 | 16 | fun onPageReceived(pages: PagedList) { 17 | val page = currentPage 18 | currentPage++ 19 | hasNextPage = pages.hasNextPage && pages.list.isNotEmpty() 20 | results.accept(Pair(page, pages.list)) 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/di/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.di 2 | 3 | object AppComponent { 4 | 5 | fun modules() = listOf(AppModule.appModule, 6 | DatabaseModule.databseModule) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.di 2 | 3 | import org.koin.android.ext.koin.androidApplication 4 | import org.koin.dsl.module.module 5 | import top.rechinx.meow.core.extension.ExtensionManager 6 | import top.rechinx.meow.core.network.NetworkHelper 7 | import top.rechinx.meow.core.source.SourceManager 8 | import top.rechinx.meow.data.cache.ChapterCache 9 | import top.rechinx.meow.data.cache.CoverCache 10 | import top.rechinx.meow.data.preference.PreferenceHelper 11 | 12 | object AppModule { 13 | 14 | val appModule = module(createOnStart = true) { 15 | single { NetworkHelper(androidApplication()) } 16 | single { SourceManager(androidApplication()) } 17 | single { PreferenceHelper(androidApplication())} 18 | single { ChapterCache(androidApplication()) } 19 | single { CoverCache(androidApplication()) } 20 | single { ExtensionManager(androidApplication(), get()) } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.di 2 | 3 | import androidx.room.Room 4 | import org.koin.android.ext.koin.androidApplication 5 | import org.koin.dsl.module.module 6 | import top.rechinx.meow.data.database.AppDatabase 7 | import top.rechinx.meow.data.database.dao.MangaDao 8 | import top.rechinx.meow.data.repository.ChapterRepository 9 | import top.rechinx.meow.data.repository.MangaRepository 10 | import top.rechinx.meow.global.Constants 11 | 12 | object DatabaseModule { 13 | 14 | val databseModule = module(createOnStart = true) { 15 | single { 16 | Room.databaseBuilder(androidApplication(), AppDatabase::class.java, Constants.DATABASE_NAME) 17 | .allowMainThreadQueries() 18 | .build() 19 | } 20 | single { get().mangaDao() } 21 | single { get().chapterDao() } 22 | single { get().taskDao() } 23 | single { MangaRepository(get(), get(), get()) } 24 | single { ChapterRepository(get(), get()) } 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/exception/NoMoreResultException.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.exception 2 | 3 | import java.lang.Exception 4 | 5 | class NoMoreResultException: Exception() -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/glide/FileFetcher.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.glide 2 | 3 | import android.content.ContentValues.TAG 4 | import android.util.Log 5 | import com.bumptech.glide.Priority 6 | import com.bumptech.glide.load.DataSource 7 | import com.bumptech.glide.load.data.DataFetcher 8 | import java.io.* 9 | 10 | open class FileFetcher(private val file: File) : DataFetcher { 11 | 12 | private var data: InputStream? = null 13 | 14 | override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { 15 | loadFromFile(callback) 16 | } 17 | 18 | protected fun loadFromFile(callback: DataFetcher.DataCallback) { 19 | try { 20 | data = FileInputStream(file) 21 | } catch (e: FileNotFoundException) { 22 | if (Log.isLoggable(TAG, Log.DEBUG)) { 23 | Log.d(TAG, "Failed to open file", e) 24 | } 25 | callback.onLoadFailed(e) 26 | return 27 | } 28 | 29 | callback.onDataReady(data) 30 | } 31 | 32 | override fun cleanup() { 33 | try { 34 | data?.close() 35 | } catch (e: IOException) { 36 | // Ignored. 37 | } 38 | } 39 | 40 | override fun cancel() { 41 | // Do nothing. 42 | } 43 | 44 | override fun getDataClass(): Class { 45 | return InputStream::class.java 46 | } 47 | 48 | override fun getDataSource(): DataSource { 49 | return DataSource.LOCAL 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/glide/GlideModule.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.glide 2 | 3 | import android.content.Context 4 | import com.bumptech.glide.Glide 5 | import com.bumptech.glide.Registry 6 | import com.bumptech.glide.annotation.GlideModule 7 | import com.bumptech.glide.module.AppGlideModule 8 | import top.rechinx.meow.data.database.model.Manga 9 | import java.io.InputStream 10 | 11 | @GlideModule 12 | class GlideModule: AppGlideModule() { 13 | 14 | override fun registerComponents(context: Context, glide: Glide, registry: Registry) { 15 | registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory()) 16 | registry.append(InputStream::class.java, InputStream::class.java, PassthroughModelLoader 17 | .Factory()) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/glide/MangaSignature.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.glide 2 | 3 | import com.bumptech.glide.load.Key 4 | import top.rechinx.meow.data.database.model.Manga 5 | import java.io.File 6 | import java.security.MessageDigest 7 | 8 | class MangaSignature(manga: Manga, file: File) : Key { 9 | 10 | private val key = manga.thumbnail_url + file.lastModified() 11 | 12 | override fun equals(other: Any?): Boolean { 13 | return if (other is MangaSignature) { 14 | key == other.key 15 | } else { 16 | false 17 | } 18 | } 19 | 20 | override fun hashCode(): Int { 21 | return key.hashCode() 22 | } 23 | 24 | override fun updateDiskCacheKey(md: MessageDigest) { 25 | md.update(key.toByteArray(Key.CHARSET)) 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/glide/PassthroughModelLoader.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.glide 2 | 3 | import com.bumptech.glide.Priority 4 | import com.bumptech.glide.load.DataSource 5 | import com.bumptech.glide.load.Options 6 | import com.bumptech.glide.load.data.DataFetcher 7 | import com.bumptech.glide.load.model.ModelLoader 8 | import com.bumptech.glide.load.model.ModelLoaderFactory 9 | import com.bumptech.glide.load.model.MultiModelLoaderFactory 10 | import com.bumptech.glide.signature.ObjectKey 11 | import java.io.IOException 12 | import java.io.InputStream 13 | 14 | class PassthroughModelLoader : ModelLoader { 15 | 16 | override fun buildLoadData( 17 | model: InputStream, 18 | width: Int, 19 | height: Int, 20 | options: Options 21 | ): ModelLoader.LoadData? { 22 | return ModelLoader.LoadData(ObjectKey(model), Fetcher(model)) 23 | } 24 | 25 | override fun handles(model: InputStream): Boolean { 26 | return true 27 | } 28 | 29 | class Fetcher(private val stream: InputStream) : DataFetcher { 30 | 31 | override fun getDataClass(): Class { 32 | return InputStream::class.java 33 | } 34 | 35 | override fun cleanup() { 36 | try { 37 | stream.close() 38 | } catch (e: IOException) { 39 | // Do nothing 40 | } 41 | } 42 | 43 | override fun getDataSource(): DataSource { 44 | return DataSource.LOCAL 45 | } 46 | 47 | override fun cancel() { 48 | // Do nothing 49 | } 50 | 51 | override fun loadData( 52 | priority: Priority, 53 | callback: DataFetcher.DataCallback 54 | ) { 55 | callback.onDataReady(stream) 56 | } 57 | 58 | } 59 | 60 | /** 61 | * Factory class for creating [PassthroughModelLoader] instances. 62 | */ 63 | class Factory : ModelLoaderFactory { 64 | 65 | override fun build( 66 | multiFactory: MultiModelLoaderFactory 67 | ): ModelLoader { 68 | return PassthroughModelLoader() 69 | } 70 | 71 | override fun teardown() {} 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/global/Constants.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.global 2 | 3 | object Constants { 4 | 5 | const val DATABASE_NAME = "meow_database" 6 | 7 | // Preference 8 | const val PREF_READER_MODE = "pref_reader_mode" 9 | const val PREF_HIDE_READER_INFO = "pref_hide_reader_status" 10 | const val PREF_FULL_SCREEN = "pref_full_screen" 11 | const val PREF_ENABLE_TRANSITIONS = "pref_enable_transitions" 12 | const val PREF_DOWNLOAD_DIRECTORY = "pref_download_directory" 13 | const val PREF_DOWNLOAD_ONLY_WIFI = "pref_download_only_wifi" 14 | const val PREF_ENABLE_VOLUME_KEYS = "pref_enable_volume_keys" 15 | const val PREF_READ_WITH_VOLUME_KEYS_INVERTED = "pref_read_with_volume_keys_inverted" 16 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/global/Extras.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.global 2 | 3 | object Extras { 4 | 5 | private const val BASE_STRING = "meow.intent.extra" 6 | const val EXTRA_KEYWORD = "$BASE_STRING.EXTRA_KEYWORD" 7 | const val EXTRA_URL = "$BASE_STRING.EXTRA_URL" 8 | const val EXTRA_SOURCE = "$BASE_STRING.EXTRA_SOURCE" 9 | const val EXTRA_CHAPTER_ID = "$BASE_STRING.EXTRA_CHAPTER_ID" 10 | const val EXTRA_MANGA_ID = "$BASE_STRING.EXTRA_MANGA_ID" 11 | const val EXTRA_CONTINUE_READ = "$BASE_STRING.EXTRA_CONTINUE_READ" 12 | const val EXTRA_CHAPTERS = "$BASE_STRING.EXTRA_CHAPTERS" 13 | const val EXTRA_TASK = "$BASE_STRING.EXTRA_TASK" 14 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.base 2 | 3 | import android.app.ActivityManager 4 | import android.os.Build 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import kotlinx.android.synthetic.main.custom_toolbar.* 8 | import top.rechinx.rikka.theme.utils.ThemeUtils 9 | import top.rechinx.meow.R.color.theme_color_primary_dark 10 | import android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS 11 | import android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 12 | import android.os.Build.VERSION.SDK_INT 13 | import android.view.WindowManager 14 | import androidx.annotation.Nullable 15 | import top.rechinx.meow.R 16 | import top.rechinx.meow.utils.ThemeHelper 17 | import top.rechinx.rikka.mvp.BaseAppCompatActivity 18 | 19 | 20 | abstract class BaseActivity: BaseAppCompatActivity() { 21 | 22 | override fun onPostCreate(@Nullable savedInstanceState: Bundle?) { 23 | super.onPostCreate(savedInstanceState) 24 | if (Build.VERSION.SDK_INT >= 21) { 25 | val window = window 26 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) 27 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) 28 | window.statusBarColor = ThemeUtils.getColorById(this, R.color.theme_color_primary_dark) 29 | val description = ActivityManager.TaskDescription(null, null, 30 | ThemeUtils.getThemeAttrColor(this, android.R.attr.colorPrimary)) 31 | setTaskDescription(description) 32 | } 33 | } 34 | 35 | override fun isNightTheme(): Boolean { 36 | return ThemeHelper.isNightTheme(this) 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.base 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | 9 | abstract class BaseFragment: Fragment() { 10 | 11 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 12 | val view = inflater.inflate(getLayoutId(), container, false) 13 | return view 14 | } 15 | 16 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 17 | super.onViewCreated(view, savedInstanceState) 18 | initData() 19 | initViews() 20 | } 21 | 22 | protected abstract fun initViews() 23 | 24 | protected abstract fun getLayoutId(): Int 25 | 26 | protected open fun initData() {} 27 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/base/BaseMvpActivity.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.base 2 | 3 | import android.app.ActivityManager 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.view.WindowManager 7 | import androidx.annotation.Nullable 8 | import top.rechinx.meow.R 9 | import top.rechinx.meow.utils.ThemeHelper 10 | import top.rechinx.rikka.mvp.MvpAppCompatActivity 11 | import top.rechinx.rikka.mvp.presenter.Presenter 12 | import top.rechinx.rikka.theme.utils.ThemeUtils 13 | 14 | abstract class BaseMvpActivity

> : MvpAppCompatActivity

() { 15 | 16 | override fun onPostCreate(@Nullable savedInstanceState: Bundle?) { 17 | super.onPostCreate(savedInstanceState) 18 | if (Build.VERSION.SDK_INT >= 21) { 19 | val window = window 20 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) 21 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) 22 | window.statusBarColor = ThemeUtils.getColorById(this, R.color.theme_color_primary_dark) 23 | val description = ActivityManager.TaskDescription(null, null, 24 | ThemeUtils.getThemeAttrColor(this, android.R.attr.colorPrimary)) 25 | setTaskDescription(description) 26 | } 27 | } 28 | 29 | override fun isNightTheme(): Boolean { 30 | return ThemeHelper.isNightTheme(this) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/base/BaseMvpActivityWithoutReflection.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.base 2 | 3 | import android.app.ActivityManager 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.view.WindowManager 7 | import androidx.annotation.Nullable 8 | import top.rechinx.meow.R 9 | import top.rechinx.meow.utils.ThemeHelper 10 | import top.rechinx.rikka.mvp.MvpAppCompatActivityWithoutReflection 11 | import top.rechinx.rikka.mvp.presenter.Presenter 12 | import top.rechinx.rikka.theme.utils.ThemeUtils 13 | 14 | abstract class BaseMvpActivityWithoutReflection

> : MvpAppCompatActivityWithoutReflection

() { 15 | 16 | override fun onPostCreate(@Nullable savedInstanceState: Bundle?) { 17 | super.onPostCreate(savedInstanceState) 18 | if (Build.VERSION.SDK_INT >= 21) { 19 | val window = window 20 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) 21 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) 22 | window.statusBarColor = ThemeUtils.getColorById(this, R.color.theme_color_primary_dark) 23 | val description = ActivityManager.TaskDescription(null, null, 24 | ThemeUtils.getThemeAttrColor(this, android.R.attr.colorPrimary)) 25 | setTaskDescription(description) 26 | } 27 | } 28 | 29 | override fun isNightTheme(): Boolean { 30 | return ThemeHelper.isNightTheme(this) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/details/DetailAdapter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.details 2 | 3 | import android.content.Context 4 | import eu.davidea.flexibleadapter.FlexibleAdapter 5 | import eu.davidea.flexibleadapter.items.IFlexible 6 | import top.rechinx.meow.data.database.model.Chapter 7 | import top.rechinx.meow.data.database.model.Manga 8 | import top.rechinx.meow.ui.details.items.ChapterItem 9 | import top.rechinx.meow.ui.details.items.HeaderItem 10 | 11 | 12 | class DetailAdapter(context: Context): FlexibleAdapter>(null, context, true) { 13 | 14 | lateinit var onLoadMoreListener: OnLoadMoreListener 15 | 16 | var latestChapterId: Long = 0 17 | 18 | fun setLast(id: Long) { 19 | if(id == latestChapterId) return 20 | var tmp = latestChapterId 21 | latestChapterId = id 22 | for(i in 0 until itemCount) { 23 | val item = getItem(i) 24 | if(item is ChapterItem) { 25 | if(item.chapter.id == latestChapterId) { 26 | updateItem(i, item, null) 27 | } else if(item.chapter.id == tmp) { 28 | updateItem(i, item, null) 29 | } 30 | } 31 | } 32 | } 33 | 34 | fun setMangaLastUpdated(manga: Manga) { 35 | val headerItem = getItem(0) as HeaderItem 36 | headerItem.manga.last_update = manga.last_update 37 | updateItem(0, headerItem, null) 38 | } 39 | 40 | fun getChapters() : ArrayList { 41 | val list = ArrayList() 42 | for(i in 0 until itemCount) { 43 | val item = getItem(i) 44 | if(item is ChapterItem) { 45 | list.add(item.chapter) 46 | } 47 | } 48 | return list 49 | } 50 | 51 | interface OnLoadMoreListener { 52 | fun onLoadMore() 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/details/chapters/ChaptersAdapter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.details.chapters 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import kotlinx.android.synthetic.main.item_chapter.view.* 8 | import top.rechinx.meow.R 9 | import top.rechinx.meow.data.database.model.Chapter 10 | import top.rechinx.meow.ui.base.BaseAdapter 11 | 12 | class ChaptersAdapter(context: Context, list: ArrayList>): BaseAdapter>(context, list) { 13 | 14 | override fun getItemDecoration(): RecyclerView.ItemDecoration? = null 15 | 16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 17 | return ViewHolder(inflater.inflate(R.layout.item_chapter, parent, false)) 18 | } 19 | 20 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 21 | super.onBindViewHolder(holder, position) 22 | val item = datas[position] 23 | holder.itemView.apply { 24 | chapterButton.text = item.element.name 25 | if(item.element.download) { 26 | chapterButton.setDownload(true) 27 | chapterButton.isSelected = false 28 | } else { 29 | chapterButton.setDownload(false) 30 | chapterButton.isSelected = item.enable 31 | } 32 | } 33 | } 34 | 35 | 36 | class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) 37 | 38 | class Switcher(var element: T, var enable: Boolean) { 39 | fun switchEnable() { 40 | enable = !enable 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/details/items/ChapterItem.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.details.items 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | import eu.davidea.flexibleadapter.FlexibleAdapter 6 | import eu.davidea.flexibleadapter.items.AbstractFlexibleItem 7 | import eu.davidea.flexibleadapter.items.IFlexible 8 | import eu.davidea.viewholders.FlexibleViewHolder 9 | import kotlinx.android.synthetic.main.item_chapter.view.* 10 | import top.rechinx.meow.R 11 | import top.rechinx.meow.data.database.model.Chapter 12 | import top.rechinx.meow.ui.details.DetailAdapter 13 | 14 | class ChapterItem(val chapter: Chapter): AbstractFlexibleItem() { 15 | 16 | override fun bindViewHolder(adapter: FlexibleAdapter>?, holder: ViewHolder, position: Int, payloads: MutableList?) { 17 | holder.bindTo(chapter) 18 | } 19 | 20 | override fun equals(other: Any?): Boolean { 21 | if (this === other) return true 22 | if (other is ChapterItem) { 23 | return chapter.id == other.chapter.id 24 | } 25 | return false 26 | } 27 | 28 | override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { 29 | return ViewHolder(view, adapter as DetailAdapter) 30 | } 31 | 32 | override fun getLayoutRes(): Int = R.layout.item_chapter 33 | 34 | class ViewHolder(private val view: View, private val adapter: DetailAdapter): FlexibleViewHolder(view, adapter) { 35 | fun bindTo(chapter: Chapter) { 36 | view.chapterButton.text = chapter.name!! 37 | view.chapterButton.isSelected = adapter.latestChapterId == chapter.id 38 | view.chapterButton.setDownload(chapter.complete) 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/details/items/LoadItem.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.details.items 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | import eu.davidea.flexibleadapter.FlexibleAdapter 6 | import eu.davidea.flexibleadapter.items.AbstractFlexibleItem 7 | import eu.davidea.flexibleadapter.items.IFlexible 8 | import eu.davidea.viewholders.FlexibleViewHolder 9 | import kotlinx.android.synthetic.main.item_chapter_loadmore.view.* 10 | import top.rechinx.meow.R 11 | import top.rechinx.meow.ui.details.DetailAdapter 12 | import top.rechinx.rikka.ext.gone 13 | import top.rechinx.rikka.ext.visible 14 | 15 | 16 | class LoadItem: AbstractFlexibleItem() { 17 | 18 | private var holder: LoadItem.ViewHolder? = null 19 | 20 | var status = IDLE 21 | 22 | override fun bindViewHolder(adapter: FlexibleAdapter>, holder: LoadItem.ViewHolder, position: Int, payloads: MutableList?) { 23 | holder.itemView.apply { 24 | loadButton.setLoadColor() 25 | if (status == IDLE) { 26 | loadButton.visible() 27 | loadButton.setOnClickListener { (adapter as DetailAdapter).onLoadMoreListener.onLoadMore() } 28 | itemProgressLoad.gone() 29 | } else if (status == LOADING) { 30 | itemProgressLoad.visible() 31 | loadButton.gone() 32 | } 33 | } 34 | 35 | } 36 | 37 | override fun equals(other: Any?): Boolean { 38 | return this === other 39 | } 40 | 41 | override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { 42 | holder = ViewHolder(view, adapter as DetailAdapter) 43 | return holder!! 44 | } 45 | 46 | override fun getLayoutRes(): Int = R.layout.item_chapter_loadmore 47 | 48 | class ViewHolder(view: View, adapter: DetailAdapter): FlexibleViewHolder(view, adapter) 49 | 50 | companion object { 51 | const val IDLE = 0 52 | const val LOADING = 1 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/extension/ExtensionActivity.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.extension 2 | 3 | import android.os.Bundle 4 | import androidx.recyclerview.widget.LinearLayoutManager 5 | import androidx.recyclerview.widget.RecyclerView 6 | import kotlinx.android.synthetic.main.activity_extension.* 7 | import kotlinx.android.synthetic.main.custom_progress_bar.* 8 | import kotlinx.android.synthetic.main.custom_toolbar.* 9 | import top.rechinx.meow.R 10 | import top.rechinx.meow.core.extension.model.Extension 11 | import top.rechinx.meow.ui.base.BaseMvpActivity 12 | import top.rechinx.rikka.ext.gone 13 | import top.rechinx.rikka.mvp.MvpAppCompatActivity 14 | import top.rechinx.rikka.mvp.factory.RequiresPresenter 15 | 16 | @RequiresPresenter(ExtensionPresenter::class) 17 | class ExtensionActivity: BaseMvpActivity() { 18 | 19 | private lateinit var adapter: ExtensionAdapter 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | setContentView(R.layout.activity_extension) 24 | 25 | 26 | setSupportActionBar(customToolbar) 27 | supportActionBar?.title = getString(R.string.drawer_extension) 28 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 29 | customToolbar.setNavigationOnClickListener { finish() } 30 | 31 | adapter = ExtensionAdapter(this, ArrayList()) 32 | recyclerView.itemAnimator = null 33 | recyclerView.setHasFixedSize(true) 34 | adapter.getItemDecoration()?.let { recyclerView.addItemDecoration(it) } 35 | recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false) 36 | recyclerView.adapter = adapter 37 | customProgressBar.gone() 38 | } 39 | 40 | fun setInstalledExtensions(items: List) { 41 | adapter.addAll(items) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/extension/ExtensionAdapter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.extension 2 | 3 | import android.content.Context 4 | import android.graphics.Rect 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.recyclerview.widget.RecyclerView 8 | import kotlinx.android.synthetic.main.item_source.view.* 9 | import org.koin.standalone.KoinComponent 10 | import org.koin.standalone.inject 11 | import top.rechinx.meow.R 12 | import top.rechinx.meow.core.extension.model.Extension 13 | import top.rechinx.meow.core.source.Source 14 | import top.rechinx.meow.data.preference.PreferenceHelper 15 | import top.rechinx.meow.ui.base.BaseAdapter 16 | import top.rechinx.rikka.ext.gone 17 | 18 | class ExtensionAdapter(context: Context, list: ArrayList): BaseAdapter(context, list), KoinComponent { 19 | 20 | private lateinit var onItemCheckedListener: OnItemCheckedListener 21 | 22 | fun setOnItemCheckedlistener(listener: OnItemCheckedListener) { 23 | this.onItemCheckedListener = listener 24 | } 25 | 26 | override fun getItemDecoration(): RecyclerView.ItemDecoration? { 27 | return object : RecyclerView.ItemDecoration() { 28 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { 29 | val offset = parent.width / 90 30 | outRect.set(offset, 0, offset, (offset * 1.5).toInt()) 31 | } 32 | } 33 | } 34 | 35 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 36 | super.onBindViewHolder(holder, position) 37 | val extension = datas[position] 38 | holder.itemView.title.text = extension.name 39 | holder.itemView.switchCompat.gone() 40 | } 41 | 42 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 43 | return ViewHolder(inflater.inflate(R.layout.item_source, parent, false)) 44 | } 45 | 46 | interface OnItemCheckedListener { 47 | fun onItemCheckedListener(isChecked: Boolean, position: Int) 48 | } 49 | 50 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) 51 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/extension/ExtensionPresenter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.extension 2 | 3 | import android.os.Bundle 4 | import io.reactivex.android.schedulers.AndroidSchedulers 5 | import io.reactivex.schedulers.Schedulers 6 | import org.koin.standalone.KoinComponent 7 | import org.koin.standalone.inject 8 | import top.rechinx.meow.core.extension.ExtensionManager 9 | import top.rechinx.rikka.mvp.BasePresenter 10 | 11 | class ExtensionPresenter: BasePresenter(), KoinComponent { 12 | 13 | private val extensionManager: ExtensionManager by inject() 14 | 15 | override fun onCreate(savedState: Bundle?) { 16 | super.onCreate(savedState) 17 | 18 | extensionManager.installedExtensionsRelay 19 | .subscribeOn(Schedulers.io()) 20 | .observeOn(AndroidSchedulers.mainThread()) 21 | .subscribeLatestCache({ view, items -> 22 | view.setInstalledExtensions(items) 23 | }) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/filter/items/CheckboxItem.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.filter.items 2 | 3 | import android.view.View 4 | import android.widget.CheckBox 5 | import androidx.recyclerview.widget.RecyclerView 6 | import eu.davidea.flexibleadapter.FlexibleAdapter 7 | import eu.davidea.flexibleadapter.items.AbstractFlexibleItem 8 | import eu.davidea.flexibleadapter.items.IFlexible 9 | import eu.davidea.viewholders.FlexibleViewHolder 10 | import top.rechinx.meow.R 11 | import top.rechinx.meow.core.source.model.Filter 12 | 13 | open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem() { 14 | 15 | override fun getLayoutRes(): Int { 16 | return R.layout.item_navigation_checkbox 17 | } 18 | 19 | override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder? { 20 | return Holder(view, adapter) 21 | } 22 | 23 | override fun bindViewHolder(adapter: FlexibleAdapter>?, holder: Holder, position: Int, payloads: MutableList?) { 24 | val view = holder.check 25 | view.text = filter.name 26 | view.isChecked = filter.state 27 | holder.itemView.setOnClickListener { 28 | view.toggle() 29 | filter.state = view.isChecked 30 | } 31 | } 32 | 33 | override fun equals(other: Any?): Boolean { 34 | if (this === other) return true 35 | if (javaClass != other?.javaClass) return false 36 | return filter == (other as CheckboxItem).filter 37 | } 38 | 39 | override fun hashCode(): Int { 40 | return filter.hashCode() 41 | } 42 | 43 | class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { 44 | 45 | val check: CheckBox = itemView.findViewById(R.id.navViewItem) 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/filter/items/GroupItem.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.filter.items 2 | 3 | import android.view.View 4 | import android.widget.ImageView 5 | import android.widget.TextView 6 | import androidx.recyclerview.widget.RecyclerView 7 | import eu.davidea.flexibleadapter.FlexibleAdapter 8 | import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem 9 | import eu.davidea.flexibleadapter.items.IFlexible 10 | import eu.davidea.flexibleadapter.items.ISectionable 11 | import eu.davidea.viewholders.ExpandableViewHolder 12 | import top.rechinx.meow.R 13 | import top.rechinx.meow.core.source.model.Filter 14 | 15 | class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem>() { 16 | 17 | init { 18 | isExpanded = false 19 | } 20 | 21 | override fun getLayoutRes(): Int { 22 | return R.layout.item_navigation_group 23 | } 24 | 25 | override fun getItemViewType(): Int { 26 | return 101 27 | } 28 | 29 | override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder? { 30 | return Holder(view, adapter) 31 | } 32 | 33 | override fun bindViewHolder(adapter: FlexibleAdapter>?, holder: Holder, position: Int, payloads: MutableList?) { 34 | holder.title.text = filter.name 35 | 36 | holder.icon.setImageResource(if (isExpanded) 37 | R.drawable.ic_expand_more_black_24dp 38 | else 39 | R.drawable.ic_chevron_right_black_24dp) 40 | 41 | holder.itemView.setOnClickListener(holder) 42 | 43 | } 44 | 45 | override fun equals(other: Any?): Boolean { 46 | if (this === other) return true 47 | if (javaClass != other?.javaClass) return false 48 | return filter == (other as GroupItem).filter 49 | } 50 | 51 | override fun hashCode(): Int { 52 | return filter.hashCode() 53 | } 54 | 55 | 56 | open class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) { 57 | 58 | val title: TextView = itemView.findViewById(R.id.title) 59 | val icon: ImageView = itemView.findViewById(R.id.expandIcon) 60 | 61 | override fun shouldNotifyParentOnClick(): Boolean { 62 | return true 63 | } 64 | 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/filter/items/HeaderItem.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.filter.items 2 | 3 | import android.view.View 4 | import android.widget.TextView 5 | import androidx.recyclerview.widget.RecyclerView 6 | import eu.davidea.flexibleadapter.FlexibleAdapter 7 | import eu.davidea.flexibleadapter.items.AbstractHeaderItem 8 | import eu.davidea.flexibleadapter.items.IFlexible 9 | import eu.davidea.viewholders.FlexibleViewHolder 10 | import top.rechinx.meow.core.source.model.Filter 11 | 12 | 13 | class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem() { 14 | 15 | override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: MutableList?) { 16 | val view = holder.itemView as TextView 17 | view.text = filter.name 18 | } 19 | 20 | override fun equals(other: Any?): Boolean { 21 | if (this === other) return true 22 | if (javaClass != other?.javaClass) return false 23 | return filter == (other as HeaderItem).filter 24 | } 25 | 26 | override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { 27 | return Holder(view, adapter) 28 | } 29 | 30 | override fun getLayoutRes(): Int { 31 | return com.google.android.material.R.layout.design_navigation_item_subheader 32 | } 33 | 34 | 35 | class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/filter/items/ProgressItem.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.filter.items 2 | 3 | import android.view.View 4 | import android.widget.ProgressBar 5 | import android.widget.TextView 6 | import androidx.recyclerview.widget.RecyclerView 7 | import eu.davidea.flexibleadapter.FlexibleAdapter 8 | import eu.davidea.flexibleadapter.items.AbstractFlexibleItem 9 | import eu.davidea.flexibleadapter.items.IFlexible 10 | import eu.davidea.viewholders.FlexibleViewHolder 11 | import kotlinx.android.synthetic.main.item_progress.view.* 12 | import top.rechinx.meow.R 13 | 14 | 15 | class ProgressItem : AbstractFlexibleItem() { 16 | 17 | private var loadMore = true 18 | 19 | override fun getLayoutRes(): Int { 20 | return R.layout.item_progress 21 | } 22 | 23 | override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder? { 24 | return ViewHolder(view, adapter) 25 | } 26 | 27 | override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ViewHolder, position: Int, payloads: MutableList) { 28 | holder.itemView.progressBar.visibility = View.GONE 29 | holder.itemView.progressMessage.visibility = View.GONE 30 | 31 | if (!adapter.isEndlessScrollEnabled) { 32 | loadMore = false 33 | } 34 | 35 | if (loadMore) { 36 | holder.itemView.progressBar.visibility = View.VISIBLE 37 | } else { 38 | holder.itemView.progressMessage.visibility = View.VISIBLE 39 | } 40 | } 41 | 42 | override fun equals(other: Any?): Boolean { 43 | return this === other 44 | } 45 | 46 | class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) 47 | 48 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/filter/items/SeparatorItem.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.filter.items 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.View 5 | import androidx.recyclerview.widget.RecyclerView 6 | import eu.davidea.flexibleadapter.FlexibleAdapter 7 | import eu.davidea.flexibleadapter.items.AbstractHeaderItem 8 | import eu.davidea.flexibleadapter.items.IFlexible 9 | import eu.davidea.viewholders.FlexibleViewHolder 10 | import top.rechinx.meow.core.source.model.Filter 11 | 12 | class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem() { 13 | 14 | @SuppressLint("PrivateResource") 15 | override fun getLayoutRes(): Int { 16 | return com.google.android.material.R.layout.design_navigation_item_separator 17 | } 18 | 19 | override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder? { 20 | return Holder(view, adapter) 21 | } 22 | 23 | override fun bindViewHolder(adapter: FlexibleAdapter>?, holder: Holder?, position: Int, payloads: MutableList?) { 24 | 25 | } 26 | 27 | override fun equals(other: Any?): Boolean { 28 | if (this === other) return true 29 | if (javaClass != other?.javaClass) return false 30 | return filter == (other as SeparatorItem).filter 31 | } 32 | 33 | override fun hashCode(): Int { 34 | return filter.hashCode() 35 | } 36 | 37 | class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) 38 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/filter/items/SortGroup.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.filter.items 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | import eu.davidea.flexibleadapter.FlexibleAdapter 6 | import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem 7 | import eu.davidea.flexibleadapter.items.IFlexible 8 | import eu.davidea.flexibleadapter.items.ISectionable 9 | import top.rechinx.meow.R 10 | import top.rechinx.meow.core.source.model.Filter 11 | 12 | class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem>() { 13 | 14 | init { 15 | isExpanded = false 16 | } 17 | 18 | override fun getLayoutRes(): Int { 19 | return R.layout.item_navigation_group 20 | } 21 | 22 | override fun getItemViewType(): Int { 23 | return 100 24 | } 25 | 26 | override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder? { 27 | return Holder(view, adapter) 28 | } 29 | 30 | override fun bindViewHolder(adapter: FlexibleAdapter>?, holder: Holder, position: Int, payloads: MutableList?) { 31 | holder.title.text = filter.name 32 | 33 | holder.icon.setImageResource(if (isExpanded) 34 | R.drawable.ic_expand_more_white_24dp 35 | else 36 | R.drawable.ic_chevron_right_white_24dp) 37 | 38 | holder.itemView.setOnClickListener(holder) 39 | 40 | } 41 | 42 | override fun equals(other: Any?): Boolean { 43 | if (this === other) return true 44 | if (javaClass != other?.javaClass) return false 45 | return filter == (other as SortGroup).filter 46 | } 47 | 48 | override fun hashCode(): Int { 49 | return filter.hashCode() 50 | } 51 | 52 | class Holder(view: View, adapter: FlexibleAdapter<*>) : GroupItem.Holder(view, adapter) 53 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/filter/items/TextItem.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.filter.items 2 | 3 | import android.view.View 4 | import android.widget.EditText 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.google.android.material.textfield.TextInputLayout 7 | import eu.davidea.flexibleadapter.FlexibleAdapter 8 | import eu.davidea.flexibleadapter.items.AbstractFlexibleItem 9 | import eu.davidea.flexibleadapter.items.IFlexible 10 | import eu.davidea.viewholders.FlexibleViewHolder 11 | import kotlinx.android.synthetic.main.item_navigation_text.view.* 12 | import top.rechinx.meow.R 13 | import top.rechinx.meow.core.source.model.Filter 14 | import top.rechinx.meow.widget.SimpleTextWatcher 15 | 16 | open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem() { 17 | 18 | override fun getLayoutRes(): Int { 19 | return R.layout.item_navigation_text 20 | } 21 | 22 | override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder? { 23 | return Holder(view, adapter) 24 | } 25 | 26 | override fun bindViewHolder(adapter: FlexibleAdapter>?, holder: Holder, position: Int, payloads: MutableList?) { 27 | holder.itemView.apply { 28 | navViewItemWrapper.hint = filter.name 29 | navViewItem.setText(filter.name) 30 | navViewItem.addTextChangedListener(object : SimpleTextWatcher() { 31 | override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { 32 | filter.state = s.toString() 33 | } 34 | }) 35 | } 36 | } 37 | 38 | override fun equals(other: Any?): Boolean { 39 | if (this === other) return true 40 | if (javaClass != other?.javaClass) return false 41 | return filter == (other as TextItem).filter 42 | } 43 | 44 | override fun hashCode(): Int { 45 | return filter.hashCode() 46 | } 47 | 48 | class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) 49 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/grid/GridAdapter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.grid 2 | 3 | import android.content.Context 4 | import eu.davidea.flexibleadapter.FlexibleAdapter 5 | import top.rechinx.meow.ui.grid.items.GridItem 6 | 7 | class GridAdapter(context: Context): FlexibleAdapter(null, context, true) { 8 | 9 | fun removeItemByMangaId(mangaId: Long) { 10 | for (position in 0 until itemCount) { 11 | val item = getItem(position)?.manga ?: continue 12 | if(item.id == mangaId) { 13 | removeItem(position) 14 | break 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/grid/favorite/FavoriteFragment.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.grid.favorite 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import eu.davidea.flexibleadapter.FlexibleAdapter 8 | import kotlinx.android.synthetic.main.fragment_grid.* 9 | import timber.log.Timber 10 | import top.rechinx.meow.R 11 | import top.rechinx.meow.data.database.model.Manga 12 | import top.rechinx.meow.ui.base.BaseAdapter 13 | import top.rechinx.meow.ui.details.DetailActivity 14 | import top.rechinx.meow.ui.grid.GridAdapter 15 | import top.rechinx.meow.ui.grid.items.GridItem 16 | import top.rechinx.rikka.mvp.MvpFragment 17 | import top.rechinx.rikka.mvp.factory.RequiresPresenter 18 | 19 | @RequiresPresenter(FavoritePresenter::class) 20 | class FavoriteFragment: MvpFragment(), 21 | FlexibleAdapter.OnItemClickListener { 22 | 23 | private val adapter by lazy { GridAdapter(activity!!) } 24 | 25 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 26 | return inflater.inflate(R.layout.fragment_grid, container, false) 27 | } 28 | 29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 30 | super.onViewCreated(view, savedInstanceState) 31 | initViews() 32 | } 33 | 34 | fun initViews() { 35 | adapter.addListener(this) 36 | recyclerView.adapter = adapter 37 | presenter.load() 38 | } 39 | 40 | fun onMangasLoaded(list: List) { 41 | adapter.updateDataSet(list.map { GridItem(it) }) 42 | } 43 | 44 | fun onMangasLoadError(throwable: Throwable) { 45 | } 46 | 47 | override fun onItemClick(view: View, position: Int) : Boolean { 48 | val manga = adapter.getItem(position)?.manga ?: return false 49 | val intent = DetailActivity.createIntent(activity!!, manga.sourceId, manga.url!!) 50 | startActivity(intent) 51 | return true 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/grid/favorite/FavoritePresenter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.grid.favorite 2 | 3 | import io.reactivex.android.schedulers.AndroidSchedulers 4 | import io.reactivex.schedulers.Schedulers 5 | import org.koin.standalone.KoinComponent 6 | import org.koin.standalone.inject 7 | import top.rechinx.meow.data.repository.MangaRepository 8 | import top.rechinx.rikka.mvp.BasePresenter 9 | 10 | class FavoritePresenter: BasePresenter(), KoinComponent { 11 | 12 | private val mangaRepository: MangaRepository by inject() 13 | 14 | fun load() { 15 | mangaRepository.listFavorite() 16 | .subscribeOn(Schedulers.io()) 17 | .observeOn(AndroidSchedulers.mainThread()) 18 | .toObservable() 19 | .subscribeLatestCache({ view, manga -> 20 | view.onMangasLoaded(manga) 21 | }, FavoriteFragment::onMangasLoadError) 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/grid/history/HistoryPresenter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.grid.history 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.android.schedulers.AndroidSchedulers 5 | import io.reactivex.schedulers.Schedulers 6 | import org.koin.standalone.KoinComponent 7 | import org.koin.standalone.inject 8 | import top.rechinx.meow.data.database.dao.MangaDao 9 | import top.rechinx.meow.data.download.DownloadProvider 10 | import top.rechinx.meow.data.preference.getOrDefault 11 | import top.rechinx.meow.data.repository.MangaRepository 12 | import top.rechinx.rikka.mvp.BasePresenter 13 | 14 | class HistoryPresenter: BasePresenter(), KoinComponent { 15 | 16 | val mangaDao by inject() 17 | 18 | private val mangaRepository: MangaRepository by inject() 19 | 20 | fun load() { 21 | mangaRepository.listHistory() 22 | .subscribeOn(Schedulers.io()) 23 | .observeOn(AndroidSchedulers.mainThread()) 24 | .toObservable() 25 | .subscribeLatestCache({ view, manga -> 26 | view.onMangasLoaded(manga) 27 | }, HistoryFragment::onMangasLoadError) 28 | } 29 | 30 | fun deleteHistoryManga(mangaIds: List) { 31 | Observable.just(mangaIds) 32 | .doOnNext { 33 | for (id in it) { 34 | val manga = mangaDao.load(id) 35 | if(manga != null) { 36 | manga.history = false 37 | mangaDao.updateManga(manga) 38 | } 39 | } 40 | }.subscribeOn(Schedulers.io()) 41 | .observeOn(AndroidSchedulers.mainThread()) 42 | .subscribeFirst({ view, ids -> 43 | view.onHistoryDeleted(ids) 44 | }) 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/home/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.home 2 | 3 | import android.os.Bundle 4 | import com.google.android.material.tabs.TabLayout 5 | import androidx.fragment.app.Fragment 6 | import androidx.viewpager.widget.ViewPager 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.fragment.app.FragmentStatePagerAdapter 11 | import kotlinx.android.synthetic.main.fragment_home.* 12 | import top.rechinx.meow.R 13 | import top.rechinx.meow.ui.grid.download.DownloadFragment 14 | import top.rechinx.meow.ui.grid.favorite.FavoriteFragment 15 | import top.rechinx.meow.ui.grid.history.HistoryFragment 16 | 17 | class HomeFragment: Fragment() { 18 | 19 | // Instance all fragments 20 | private val fragments : Array = arrayOf(FavoriteFragment(), 21 | HistoryFragment(), 22 | DownloadFragment()) 23 | 24 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 25 | val view = inflater.inflate(R.layout.fragment_home, container, false) 26 | return view 27 | } 28 | 29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 30 | super.onViewCreated(view, savedInstanceState) 31 | val pages = resources.getStringArray(R.array.home_tabs) 32 | homeViewPager.adapter = object : FragmentStatePagerAdapter(childFragmentManager) { 33 | 34 | override fun getItem(position: Int): Fragment = fragments[position] 35 | 36 | override fun getCount(): Int = fragments.size 37 | 38 | override fun getPageTitle(position: Int): CharSequence = pages[position] 39 | } 40 | homeTabLayout.setupWithViewPager(homeViewPager) 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/loader/DownloadedPageLoader.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.loader 2 | 3 | import android.app.Application 4 | import io.reactivex.Observable 5 | import org.koin.standalone.KoinComponent 6 | import org.koin.standalone.inject 7 | import top.rechinx.meow.core.source.HttpSource 8 | import top.rechinx.meow.core.source.model.MangaPage 9 | import top.rechinx.meow.data.database.model.Manga 10 | import top.rechinx.meow.data.download.DownloadProvider 11 | import top.rechinx.meow.data.preference.PreferenceHelper 12 | import top.rechinx.meow.data.preference.getOrDefault 13 | import top.rechinx.meow.ui.reader.model.ReaderChapter 14 | import top.rechinx.meow.ui.reader.model.ReaderPage 15 | 16 | class DownloadedPageLoader(private val chapter: ReaderChapter, 17 | private val manga: Manga, 18 | private val source: HttpSource) : PageLoader(), KoinComponent { 19 | 20 | val preferences by inject() 21 | val context by inject () 22 | 23 | override fun getPages(): Observable> { 24 | return DownloadProvider.buildReaderPages(source, manga, chapter.chapter, 25 | preferences.downloadsDirectory().getOrDefault(), 26 | context.contentResolver) 27 | } 28 | 29 | override fun getPage(page: ReaderPage): Observable { 30 | return Observable.just(MangaPage.READY) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/loader/PageLoader.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.loader 2 | 3 | import androidx.annotation.CallSuper 4 | import io.reactivex.Observable 5 | import top.rechinx.meow.ui.reader.model.ReaderPage 6 | 7 | abstract class PageLoader { 8 | 9 | var isRecycled = false 10 | private set 11 | 12 | @CallSuper 13 | open fun recycle() { 14 | isRecycled = false 15 | } 16 | 17 | abstract fun getPages(): Observable> 18 | 19 | abstract fun getPage(page: ReaderPage): Observable 20 | 21 | open fun retryPage(page: ReaderPage) {} 22 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/model/ChapterTransition.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.model 2 | 3 | sealed class ChapterTransition { 4 | 5 | abstract val from: ReaderChapter 6 | abstract val to: ReaderChapter? 7 | 8 | class Prev( 9 | override val from: ReaderChapter, override val to: ReaderChapter? 10 | ) : ChapterTransition() 11 | class Next( 12 | override val from: ReaderChapter, override val to: ReaderChapter? 13 | ) : ChapterTransition() 14 | 15 | override fun equals(other: Any?): Boolean { 16 | if (this === other) return true 17 | if (other !is ChapterTransition) return false 18 | if (from == other.from && to == other.to) return true 19 | if (from == other.to && to == other.from) return true 20 | return false 21 | } 22 | 23 | override fun hashCode(): Int { 24 | var result = from.hashCode() 25 | result = 31 * result + (to?.hashCode() ?: 0) 26 | return result 27 | } 28 | 29 | override fun toString(): String { 30 | return "${javaClass.simpleName}(from=${from.chapter.url}, to=${to?.chapter?.url})" 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/model/ReaderChapter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.model 2 | 3 | import com.jakewharton.rxrelay2.BehaviorRelay 4 | import top.rechinx.meow.data.database.model.Chapter 5 | import top.rechinx.meow.ui.reader.loader.PageLoader 6 | 7 | data class ReaderChapter(val chapter: Chapter) { 8 | 9 | var state: State = 10 | State.Wait 11 | set(value) { 12 | field = value 13 | stateRelay.accept(value) 14 | } 15 | 16 | private val stateRelay by lazy { BehaviorRelay.createDefault(state) } 17 | 18 | val stateObserver by lazy { stateRelay } 19 | 20 | val pages: List? 21 | get() = (state as? State.Loaded)?.pages 22 | 23 | var pageLoader: PageLoader? = null 24 | 25 | var requestedPage: Int = 0 26 | 27 | var references = 0 28 | private set 29 | 30 | fun ref() { 31 | references++ 32 | } 33 | 34 | fun unref() { 35 | references-- 36 | if (references == 0) { 37 | pageLoader?.recycle() 38 | pageLoader = null 39 | state = State.Wait 40 | } 41 | } 42 | 43 | sealed class State { 44 | object Wait : State() 45 | object Loading : State() 46 | class Error(val error: Throwable) : State() 47 | class Loaded(val pages: List) : State() 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/model/ReaderPage.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.model 2 | 3 | import top.rechinx.meow.core.source.model.MangaPage 4 | import java.io.InputStream 5 | 6 | class ReaderPage(index: Int, 7 | url: String, 8 | imageUrl: String?, 9 | var stream: (() -> InputStream)? = null): MangaPage(index, url, imageUrl) { 10 | lateinit var chapter: ReaderChapter 11 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/model/ViewerChapters.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.model 2 | 3 | data class ViewerChapters( 4 | val currChapter: ReaderChapter, 5 | val prevChapter: ReaderChapter?, 6 | val nextChapter: ReaderChapter? 7 | ) { 8 | fun ref() { 9 | currChapter.ref() 10 | prevChapter?.ref() 11 | nextChapter?.ref() 12 | } 13 | 14 | fun unref() { 15 | currChapter.unref() 16 | prevChapter?.unref() 17 | nextChapter?.unref() 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/viewer/BaseViewer.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.viewer 2 | 3 | import android.view.KeyEvent 4 | import android.view.MotionEvent 5 | import android.view.View 6 | import top.rechinx.meow.ui.reader.model.ReaderPage 7 | import top.rechinx.meow.ui.reader.model.ViewerChapters 8 | 9 | /** 10 | * Interface for implementing a viewer. 11 | */ 12 | interface BaseViewer { 13 | 14 | /** 15 | * Returns the view this viewer uses. 16 | */ 17 | fun getView(): View 18 | 19 | /** 20 | * Destroys this viewer. Called when leaving the reader or swapping viewers. 21 | */ 22 | fun destroy() {} 23 | 24 | /** 25 | * Tells this viewer to set the given [chapters] as active. 26 | */ 27 | fun setChapters(chapters: ViewerChapters) 28 | 29 | /** 30 | * Tells this viewer to move to the given [page]. 31 | */ 32 | fun moveToPage(page: ReaderPage) 33 | 34 | /** 35 | * Called from the containing activity when a key [event] is received. It should return true 36 | * if the event was handled, false otherwise. 37 | */ 38 | fun handleKeyEvent(event: KeyEvent): Boolean 39 | 40 | /** 41 | * Called from the containing activity when a generic motion [event] is received. It should 42 | * return true if the event was handled, false otherwise. 43 | */ 44 | fun handleGenericMotionEvent(event: MotionEvent): Boolean 45 | 46 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/viewer/pager/PagerButton.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.viewer.pager 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import androidx.appcompat.widget.AppCompatButton 6 | import android.view.MotionEvent 7 | import com.google.android.material.button.MaterialButton 8 | 9 | /** 10 | * A button class to be used by child views of the pager viewer. All tap gestures are handled by 11 | * the pager, but this class disables that behavior to allow clickable buttons. 12 | */ 13 | @SuppressLint("ViewConstructor") 14 | class PagerButton(context: Context, viewer: PagerViewer) : MaterialButton(context) { 15 | 16 | init { 17 | setOnTouchListener { _, event -> 18 | viewer.pager.setGestureDetectorEnabled(false) 19 | if (event.actionMasked == MotionEvent.ACTION_UP) { 20 | viewer.pager.setGestureDetectorEnabled(true) 21 | } 22 | false 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/viewer/pager/PagerConfig.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.viewer.pager 2 | 3 | import com.f2prateek.rx.preferences2.Preference 4 | import io.reactivex.disposables.CompositeDisposable 5 | import io.reactivex.rxkotlin.addTo 6 | import org.koin.standalone.KoinComponent 7 | import org.koin.standalone.inject 8 | import top.rechinx.meow.data.preference.PreferenceHelper 9 | 10 | class PagerConfig(private val viewer: PagerViewer) : KoinComponent { 11 | 12 | private val preference: PreferenceHelper by inject() 13 | private val disposables = CompositeDisposable() 14 | 15 | var usePageTransitions = false 16 | private set 17 | 18 | var volumeKeysEnabled = false 19 | private set 20 | 21 | var volumeKeysInverted = false 22 | private set 23 | 24 | init { 25 | preference.pageTransitions() 26 | .register({ usePageTransitions = it }) 27 | 28 | preference.enableVolumeKeys() 29 | .register({ volumeKeysEnabled = it }) 30 | 31 | preference.readWithVolumeKeysInverted() 32 | .register({ volumeKeysInverted = it }) 33 | } 34 | 35 | private fun Preference.register( 36 | valueAssignment: (T) -> Unit, 37 | onChanged: (T) -> Unit = {} 38 | ) { 39 | asObservable() 40 | .doOnNext(valueAssignment) 41 | .skip(1) 42 | .distinctUntilChanged() 43 | .doOnNext(onChanged) 44 | .subscribe() 45 | .addTo(disposables) 46 | } 47 | 48 | fun clear() { 49 | disposables.clear() 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/viewer/pager/PagerViewers.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.viewer.pager 2 | 3 | import top.rechinx.meow.ui.reader.ReaderActivity 4 | 5 | class L2RPagerViewer(activity: ReaderActivity) : PagerViewer(activity) { 6 | 7 | override fun createPager(): Pager = Pager(activity) 8 | 9 | } 10 | 11 | class R2LPagerViewer(activity: ReaderActivity): PagerViewer(activity) { 12 | 13 | override fun createPager(): Pager = Pager(activity) 14 | 15 | /** 16 | * Moves to the next page. On a R2L pager the next page is the one at the left. 17 | */ 18 | override fun moveToNext() { 19 | moveLeft() 20 | } 21 | 22 | /** 23 | * Moves to the previous page. On a R2L pager the previous page is the one at the right. 24 | */ 25 | override fun moveToPrevious() { 26 | moveRight() 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/viewer/pager/ViewPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.viewer.pager 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import androidx.core.view.PagerAdapter 6 | 7 | abstract class ViewPagerAdapter : PagerAdapter() { 8 | 9 | protected abstract fun createView(container: ViewGroup, position: Int): View 10 | 11 | protected open fun destroyView(container: ViewGroup, position: Int, view: View) { 12 | } 13 | 14 | override fun instantiateItem(container: ViewGroup, position: Int): Any { 15 | val view = createView(container, position) 16 | container.addView(view) 17 | return view 18 | } 19 | 20 | override fun destroyItem(container: ViewGroup, position: Int, obj: Any) { 21 | val view = obj as View 22 | destroyView(container, position, view) 23 | container.removeView(view) 24 | } 25 | 26 | override fun isViewFromObject(view: View, obj: Any): Boolean { 27 | return view === obj 28 | } 29 | 30 | interface PositionableView { 31 | val item: Any 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.viewer.webtoon 2 | 3 | import android.content.Context 4 | import androidx.recyclerview.widget.RecyclerView 5 | import android.view.View 6 | import io.reactivex.disposables.Disposable 7 | 8 | abstract class WebtoonBaseHolder( 9 | view: View, 10 | protected val viewer: WebtoonViewer 11 | ) : RecyclerView.ViewHolder(view) { 12 | 13 | val context: Context get() = itemView.context 14 | 15 | open fun recycle() {} 16 | 17 | protected fun addDispoable(disposable: Disposable?) { 18 | disposable?.let { viewer.disposables.add(it) } 19 | } 20 | 21 | protected fun removeDisposable(disposable: Disposable?) { 22 | disposable?.let { viewer.disposables.remove(it) } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/viewer/webtoon/WebtoonConfig.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.viewer.webtoon 2 | 3 | import com.f2prateek.rx.preferences2.Preference 4 | import io.reactivex.disposables.CompositeDisposable 5 | import io.reactivex.rxkotlin.addTo 6 | import org.koin.standalone.KoinComponent 7 | import org.koin.standalone.inject 8 | import top.rechinx.meow.data.preference.PreferenceHelper 9 | 10 | class WebtoonConfig: KoinComponent { 11 | 12 | private val preference: PreferenceHelper by inject() 13 | 14 | private val disposables = CompositeDisposable() 15 | 16 | var volumeKeysEnabled = false 17 | private set 18 | 19 | var volumeKeysInverted = false 20 | private set 21 | 22 | init { 23 | preference.enableVolumeKeys() 24 | .register({ volumeKeysEnabled = it }) 25 | 26 | preference.readWithVolumeKeysInverted() 27 | .register({ volumeKeysInverted = it }) 28 | } 29 | 30 | fun unsubscribe() { 31 | disposables.clear() 32 | } 33 | 34 | private fun Preference.register( 35 | valueAssignment: (T) -> Unit, 36 | onChanged: (T) -> Unit = {} 37 | ) { 38 | asObservable() 39 | .doOnNext(valueAssignment) 40 | .skip(1) 41 | .distinctUntilChanged() 42 | .doOnNext(onChanged) 43 | .subscribe() 44 | .addTo(disposables) 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/viewer/webtoon/WebtoonFrame.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.reader.viewer.webtoon 2 | 3 | import android.content.Context 4 | import android.view.GestureDetector 5 | import android.view.MotionEvent 6 | import android.view.ScaleGestureDetector 7 | import android.widget.FrameLayout 8 | 9 | class WebtoonFrame(context: Context): FrameLayout(context) { 10 | 11 | private val recycler: WebtoonRecyclerView? 12 | get() = getChildAt(0) as? WebtoonRecyclerView 13 | 14 | private val scaleDetector = ScaleGestureDetector(context, ScaleListener()) 15 | 16 | private val flingDetector = GestureDetector(context, FlingListener()) 17 | 18 | override fun dispatchTouchEvent(ev: MotionEvent): Boolean { 19 | scaleDetector.onTouchEvent(ev) 20 | flingDetector.onTouchEvent(ev) 21 | return super.dispatchTouchEvent(ev) 22 | } 23 | 24 | inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() { 25 | override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean { 26 | recycler?.onScaleBegin() 27 | return true 28 | } 29 | 30 | override fun onScale(detector: ScaleGestureDetector): Boolean { 31 | recycler?.onScale(detector.scaleFactor) 32 | return true 33 | } 34 | 35 | override fun onScaleEnd(detector: ScaleGestureDetector) { 36 | recycler?.onScaleEnd() 37 | } 38 | } 39 | 40 | inner class FlingListener : GestureDetector.SimpleOnGestureListener() { 41 | override fun onDown(e: MotionEvent?): Boolean { 42 | return true 43 | } 44 | 45 | override fun onFling( 46 | e1: MotionEvent?, 47 | e2: MotionEvent?, 48 | velocityX: Float, 49 | velocityY: Float 50 | ): Boolean { 51 | return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PackageDirectoryMismatch") 2 | package androidx.recyclerview.widget 3 | 4 | import androidx.recyclerview.widget.LinearLayoutManager 5 | import androidx.recyclerview.widget.RecyclerView 6 | import top.rechinx.meow.ui.reader.ReaderActivity 7 | 8 | class WebtoonLayoutManager(activity: ReaderActivity) : LinearLayoutManager(activity) { 9 | 10 | private val extraLayoutSpace = activity.resources.displayMetrics.heightPixels / 2 11 | 12 | init { 13 | isItemPrefetchEnabled = false 14 | } 15 | 16 | /** 17 | * Returns the custom extra layout space. 18 | */ 19 | override fun getExtraLayoutSpace(state: RecyclerView.State): Int { 20 | return extraLayoutSpace 21 | } 22 | 23 | /** 24 | * Returns the position of the last item whose end side is visible on screen. 25 | */ 26 | fun findLastEndVisibleItemPosition(): Int { 27 | ensureLayoutState() 28 | @ViewBoundsCheck.ViewBounds val preferredBoundsFlag = 29 | (ViewBoundsCheck.FLAG_CVE_LT_PVE or ViewBoundsCheck.FLAG_CVE_EQ_PVE) 30 | 31 | val fromIndex = childCount - 1 32 | val toIndex = -1 33 | 34 | val child = if (mOrientation == HORIZONTAL) 35 | mHorizontalBoundCheck 36 | .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, 0) 37 | else 38 | mVerticalBoundCheck 39 | .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, 0) 40 | 41 | return if (child == null) RecyclerView.NO_POSITION else getPosition(child) 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/result/ResultAdapter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.result 2 | 3 | import android.content.Context 4 | import androidx.recyclerview.widget.RecyclerView 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import kotlinx.android.synthetic.main.item_result.view.* 8 | import top.rechinx.meow.R 9 | import top.rechinx.meow.data.database.model.Manga 10 | import top.rechinx.meow.ui.base.BaseAdapter 11 | import top.rechinx.meow.glide.GlideApp 12 | 13 | class ResultAdapter: BaseAdapter { 14 | 15 | 16 | constructor(context: Context, list: ArrayList): super(context, list) 17 | 18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 19 | return ViewHolder(inflater.inflate(R.layout.item_result, parent, false)) 20 | } 21 | 22 | override fun getItemCount(): Int = datas.size 23 | 24 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 25 | super.onBindViewHolder(holder, position) 26 | val manga = datas[position] 27 | val itemHolder = holder as ViewHolder 28 | 29 | itemHolder.itemView.apply { 30 | title.text = manga.title 31 | author.text = if(manga.author.isNullOrEmpty()) context.getString(R.string.unknown) else manga.author 32 | source.text = manga.sourceName 33 | GlideApp.with(context) 34 | .load(manga) 35 | .centerCrop() 36 | .circleCrop() 37 | .into(thumbnail) 38 | } 39 | } 40 | 41 | override fun getItemDecoration(): RecyclerView.ItemDecoration? = null 42 | 43 | class ViewHolder(view: View): RecyclerView.ViewHolder(view) 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/setting/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.setting 2 | 3 | import android.os.Bundle 4 | import kotlinx.android.synthetic.main.custom_toolbar.* 5 | import top.rechinx.meow.R 6 | import top.rechinx.meow.ui.base.BaseActivity 7 | 8 | class SettingsActivity: BaseActivity() { 9 | 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | setContentView(R.layout.activity_settings) 13 | setSupportActionBar(customToolbar) 14 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 15 | supportActionBar?.title = getString(R.string.title_activity_settings) 16 | 17 | customToolbar?.setNavigationOnClickListener { finish() } 18 | val fragmentTransaction = supportFragmentManager.beginTransaction() 19 | fragmentTransaction.replace(R.id.frameContainer, MainSettingsFragment()).commit() 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/ui/setting/ThemeAdapter.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.ui.setting 2 | 3 | import android.content.Context 4 | import android.graphics.Rect 5 | import android.graphics.drawable.ColorDrawable 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.recyclerview.widget.RecyclerView 9 | import kotlinx.android.synthetic.main.item_theme.view.* 10 | import org.koin.standalone.KoinComponent 11 | import top.rechinx.meow.R 12 | import top.rechinx.meow.ui.base.BaseAdapter 13 | import top.rechinx.meow.utils.ThemeHelper 14 | import top.rechinx.rikka.ext.gone 15 | import top.rechinx.rikka.ext.visible 16 | 17 | class ThemeAdapter(context: Context, list: ArrayList>): BaseAdapter>(context, list), KoinComponent { 18 | 19 | override fun getItemDecoration(): RecyclerView.ItemDecoration? { 20 | return null 21 | } 22 | 23 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 24 | super.onBindViewHolder(holder, position) 25 | val theme = datas[position] 26 | holder.itemView.apply { 27 | itemColor.background = ColorDrawable(theme.second) 28 | title.text = theme.third 29 | if (ThemeHelper.getTheme(context) == theme.first) { 30 | selected.visible() 31 | } else { 32 | selected.gone() 33 | } 34 | } 35 | } 36 | 37 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 38 | return ViewHolder(inflater.inflate(R.layout.item_theme, parent, false)) 39 | } 40 | 41 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) 42 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/utils/DiskUtil.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.utils 2 | 3 | import java.io.File 4 | 5 | object DiskUtil { 6 | 7 | fun hashKeyForDisk(key: String): String { 8 | return Hash.md5(key) 9 | } 10 | 11 | fun getDirectorySize(f: File): Long { 12 | var size: Long = 0 13 | if (f.isDirectory) { 14 | for (file in f.listFiles()) { 15 | size += getDirectorySize(file) 16 | } 17 | } else { 18 | size = f.length() 19 | } 20 | return size 21 | } 22 | 23 | /** 24 | * Mutate the given filename to make it valid for a FAT filesystem, 25 | * replacing any invalid characters with "_". This method doesn't allow hidden files (starting 26 | * with a dot), but you can manually add it later. 27 | */ 28 | fun buildValidFilename(origName: String): String { 29 | val name = origName.trim('.', ' ') 30 | if (name.isNullOrEmpty()) { 31 | return "(invalid)" 32 | } 33 | val sb = StringBuilder(name.length) 34 | name.forEach { c -> 35 | if (isValidFatFilenameChar(c)) { 36 | sb.append(c) 37 | } else { 38 | sb.append('_') 39 | } 40 | } 41 | // Even though vfat allows 255 UCS-2 chars, we might eventually write to 42 | // ext4 through a FUSE layer, so use that limit minus 15 reserved characters. 43 | return sb.toString().take(240) 44 | } 45 | 46 | /** 47 | * Returns true if the given character is a valid filename character, false otherwise. 48 | */ 49 | private fun isValidFatFilenameChar(c: Char): Boolean { 50 | if (0x00.toChar() <= c && c <= 0x1f.toChar()) { 51 | return false 52 | } 53 | return when (c) { 54 | '"', '*', '/', ':', '<', '>', '?', '\\', '|', 0x7f.toChar() -> false 55 | else -> true 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/utils/Hash.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.utils 2 | 3 | import java.security.MessageDigest 4 | 5 | object Hash { 6 | 7 | private val chars = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 8 | 'a', 'b', 'c', 'd', 'e', 'f') 9 | 10 | private val MD5 get() = MessageDigest.getInstance("MD5") 11 | 12 | private val SHA256 get() = MessageDigest.getInstance("SHA-256") 13 | 14 | fun sha256(bytes: ByteArray): String { 15 | return encodeHex(SHA256.digest(bytes)) 16 | } 17 | 18 | fun sha256(string: String): String { 19 | return sha256(string.toByteArray()) 20 | } 21 | 22 | fun md5(bytes: ByteArray): String { 23 | return encodeHex(MD5.digest(bytes)) 24 | } 25 | 26 | fun md5(string: String): String { 27 | return md5(string.toByteArray()) 28 | } 29 | 30 | private fun encodeHex(data: ByteArray): String { 31 | val l = data.size 32 | val out = CharArray(l shl 1) 33 | var i = 0 34 | var j = 0 35 | while (i < l) { 36 | out[j++] = chars[(240 and data[i].toInt()).ushr(4)] 37 | out[j++] = chars[15 and data[i].toInt()] 38 | i++ 39 | } 40 | return String(out) 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/utils/RetryWithDelay.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.utils 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.Scheduler 5 | import io.reactivex.functions.Function 6 | import io.reactivex.schedulers.Schedulers 7 | import java.util.concurrent.TimeUnit.MILLISECONDS 8 | 9 | class RetryWithDelay( 10 | private val maxRetries: Int = 1, 11 | private val retryStrategy: (Int) -> Int = { 1000 }, 12 | private val scheduler: Scheduler = Schedulers.computation() 13 | ) : Function, Observable<*>> { 14 | 15 | private var retryCount = 0 16 | 17 | override fun apply(attempts: Observable): Observable<*> = attempts.flatMap { error -> 18 | val count = ++retryCount 19 | if (count <= maxRetries) { 20 | Observable.timer(retryStrategy(count).toLong(), MILLISECONDS, scheduler) 21 | } else { 22 | Observable.error(error as Throwable) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/utils/ThemeHelper.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.utils 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | 6 | 7 | object ThemeHelper { 8 | 9 | private val CURRENT_THEME = "theme_current" 10 | 11 | const val THEME_BLUE = 0x1 12 | const val THEME_PINK = 0x2 13 | const val THEME_PURPLE = 0x3 14 | const val THEME_GREEN = 0x4 15 | const val THEME_GREEN_LIGHT = 0x5 16 | const val THEME_YELLOW = 0x6 17 | const val THEME_ORANGE = 0x7 18 | const val THEME_RED = 0x8 19 | const val THEME_NIGHT = 0x9 20 | 21 | fun getSharePreference(context: Context): SharedPreferences { 22 | return context.getSharedPreferences("multiple_theme", Context.MODE_PRIVATE) 23 | } 24 | 25 | fun setTheme(context: Context, themeId: Int) { 26 | getSharePreference(context).edit() 27 | .putInt(CURRENT_THEME, themeId) 28 | .apply() 29 | } 30 | 31 | fun getTheme(context: Context): Int { 32 | return getSharePreference(context).getInt(CURRENT_THEME, THEME_BLUE) 33 | } 34 | 35 | fun isDefaultTheme(context: Context): Boolean { 36 | return getTheme(context) == THEME_BLUE 37 | } 38 | 39 | fun isNightTheme(context: Context) : Boolean { 40 | return getTheme(context) == THEME_NIGHT 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/utils/Utility.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.utils 2 | 3 | import android.content.Context 4 | import android.graphics.Matrix 5 | import android.os.Build 6 | import android.view.View 7 | import java.text.SimpleDateFormat 8 | import java.util.* 9 | import android.util.DisplayMetrics 10 | import android.view.WindowManager 11 | 12 | 13 | 14 | object Utility { 15 | 16 | fun dpToPixel(dp: Float, context: Context): Float { 17 | return dp * context.resources.displayMetrics.density 18 | } 19 | 20 | fun getFormatTime(format: String, time: Long): String { 21 | return SimpleDateFormat(format, Locale.getDefault()).format(Date(time)) 22 | } 23 | 24 | fun calculateScale(matrix: Matrix): Float { 25 | val values = FloatArray(9) 26 | matrix.getValues(values) 27 | return Math.sqrt((Math.pow(values[Matrix.MSCALE_X].toDouble(), 2.0).toFloat() + Math.pow(values[Matrix.MSKEW_Y].toDouble(), 2.0).toFloat()).toDouble()).toFloat() 28 | } 29 | 30 | fun getViewWidth(view: View): Int { 31 | return view.width - view.paddingLeft - view.paddingRight 32 | } 33 | 34 | fun getViewHeight(view: View): Int { 35 | return view.height - view.paddingTop - view.paddingBottom 36 | } 37 | 38 | @JvmStatic fun getScreenHeight(context: Context): Int { 39 | val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 40 | val outMetrics = DisplayMetrics() 41 | wm.defaultDisplay.getMetrics(outMetrics) 42 | return outMetrics.heightPixels 43 | } 44 | 45 | @JvmStatic fun getScreenWidth(context: Context): Int { 46 | val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 47 | val outMetrics = DisplayMetrics() 48 | wm.defaultDisplay.getMetrics(outMetrics) 49 | return outMetrics.widthPixels 50 | } 51 | 52 | fun postOnAnimation(view: View, runnable: Runnable) { 53 | if (Build.VERSION.SDK_INT >= 16) { 54 | view.postOnAnimation(runnable) 55 | } else { 56 | view.postDelayed(runnable, 16L) 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/widget/AutofitRecyclerView.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.widget 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.recyclerview.widget.GridLayoutManager 6 | import androidx.recyclerview.widget.RecyclerView 7 | 8 | class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : 9 | RecyclerView(context, attrs) { 10 | 11 | private val manager = GridLayoutManager(context, 1) 12 | 13 | private var columnWidth = -1 14 | 15 | var spanCount = 0 16 | set(value) { 17 | field = value 18 | if(value > 0) { 19 | manager.spanCount = value 20 | } 21 | } 22 | 23 | val itemWidth: Int 24 | get() = measuredWidth / manager.spanCount 25 | 26 | init { 27 | if (attrs != null) { 28 | val attrsArray = intArrayOf(android.R.attr.columnWidth) 29 | val array = context.obtainStyledAttributes(attrs, attrsArray) 30 | columnWidth = array.getDimensionPixelSize(0, -1) 31 | array.recycle() 32 | } 33 | 34 | layoutManager = manager 35 | } 36 | 37 | override fun onMeasure(widthSpec: Int, heightSpec: Int) { 38 | super.onMeasure(widthSpec, heightSpec) 39 | if (spanCount == 0 && columnWidth > 0) { 40 | 41 | val count = Math.max(1, measuredWidth / columnWidth) 42 | spanCount = count 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/widget/MiniClockText.java: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.widget; 2 | 3 | import android.content.Context; 4 | import android.os.SystemClock; 5 | import androidx.appcompat.widget.AppCompatTextView; 6 | import android.text.format.DateFormat; 7 | import android.util.AttributeSet; 8 | 9 | import java.util.Calendar; 10 | 11 | /** 12 | * Created by Hiroshi on 2016/7/16. 13 | */ 14 | public class MiniClockText extends AppCompatTextView { 15 | 16 | public static final CharSequence FORMAT_24_HOUR = "HH:mm"; 17 | 18 | private Calendar mCalendar; 19 | private boolean mAttached = false; 20 | 21 | private Runnable mTicker = new Runnable() { 22 | @Override 23 | public void run() { 24 | mCalendar.setTimeInMillis(System.currentTimeMillis()); 25 | setText(DateFormat.format(FORMAT_24_HOUR, mCalendar)); 26 | 27 | long now = SystemClock.uptimeMillis(); 28 | long next = now + (1000 - now % 1000); 29 | 30 | getHandler().postAtTime(mTicker, next); 31 | } 32 | }; 33 | 34 | public MiniClockText(Context context) { 35 | this(context, null); 36 | } 37 | 38 | public MiniClockText(Context context, AttributeSet attrs) { 39 | this(context, attrs, 0); 40 | } 41 | 42 | public MiniClockText(Context context, AttributeSet attrs, int defStyle) { 43 | super(context, attrs, defStyle); 44 | initClock(); 45 | } 46 | 47 | private void initClock() { 48 | if (mCalendar == null) { 49 | mCalendar = Calendar.getInstance(); 50 | } 51 | } 52 | 53 | @Override 54 | protected void onAttachedToWindow() { 55 | super.onAttachedToWindow(); 56 | if (!mAttached) { 57 | mAttached = true; 58 | mTicker.run(); 59 | } 60 | } 61 | 62 | @Override 63 | protected void onDetachedFromWindow() { 64 | super.onDetachedFromWindow(); 65 | if (mAttached) { 66 | getHandler().removeCallbacks(mTicker); 67 | mAttached = false; 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/widget/ReverseSeekBar.java: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.widget; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | 6 | import org.adw.library.widgets.discreteseekbar.DiscreteSeekBar; 7 | 8 | /** 9 | * Created by Hiroshi on 2016/8/13. 10 | */ 11 | public class ReverseSeekBar extends DiscreteSeekBar { 12 | 13 | private boolean isReverse = false; 14 | 15 | public ReverseSeekBar(Context context, AttributeSet attrs, int defStyle) { 16 | super(context, attrs, defStyle); 17 | } 18 | 19 | public ReverseSeekBar(Context context, AttributeSet attrs) { 20 | super(context, attrs); 21 | } 22 | 23 | public ReverseSeekBar(Context context) { 24 | super(context); 25 | } 26 | 27 | @Override 28 | public boolean isRtl() { 29 | return isReverse; 30 | } 31 | 32 | public void setReverse(boolean reverse) { 33 | isReverse = reverse; 34 | invalidate(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/widget/ScrollAwareFABBehavior.java: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.widget; 2 | 3 | import android.content.Context; 4 | import androidx.coordinatorlayout.widget.CoordinatorLayout; 5 | import com.google.android.material.floatingactionbutton.FloatingActionButton; 6 | import androidx.core.view.ViewCompat; 7 | import android.util.AttributeSet; 8 | import android.view.View; 9 | 10 | /** 11 | * Created by Hiroshi on 2016/7/2. 12 | */ 13 | public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior { 14 | 15 | public ScrollAwareFABBehavior(Context context, AttributeSet attrs) { 16 | super(); 17 | } 18 | 19 | @Override 20 | public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, 21 | FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) { 22 | return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL || 23 | super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, 24 | nestedScrollAxes); 25 | } 26 | 27 | @Override 28 | public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, 29 | View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { 30 | super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, 31 | dyUnconsumed); 32 | if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) { 33 | child.hide(new FloatingActionButton.OnVisibilityChangedListener() { 34 | @Override 35 | public void onHidden(FloatingActionButton fab) { 36 | super.onHidden(fab); 37 | fab.setVisibility(View.INVISIBLE); 38 | } 39 | }); 40 | } else if (dyConsumed < 0 && child.getVisibility() == View.INVISIBLE) { 41 | child.show(); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/widget/SimpleTextWatcher.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.widget 2 | 3 | import android.text.Editable 4 | import android.text.TextWatcher 5 | 6 | open class SimpleTextWatcher : TextWatcher { 7 | override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} 8 | 9 | override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} 10 | 11 | override fun afterTextChanged(s: Editable) {} 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/top/rechinx/meow/widget/TintBottomNavigationView.java: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow.widget; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | 6 | import com.google.android.material.bottomnavigation.BottomNavigationView; 7 | 8 | import top.rechinx.meow.R; 9 | import top.rechinx.rikka.theme.utils.ThemeUtils; 10 | import top.rechinx.rikka.theme.widgets.Tintable; 11 | 12 | public class TintBottomNavigationView extends BottomNavigationView implements Tintable { 13 | 14 | public TintBottomNavigationView(Context context) { 15 | this(context, null); 16 | } 17 | 18 | public TintBottomNavigationView(Context context, AttributeSet attrs) { 19 | this(context, attrs, com.google.android.material.R.attr.bottomNavigationStyle); 20 | } 21 | 22 | public TintBottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) { 23 | super(context, attrs, defStyleAttr); 24 | setItemIconTintList(ThemeUtils.getThemeColorStateList(getContext(), R.color.bottom_navigation_colors)); 25 | setItemTextColor(ThemeUtils.getThemeColorStateList(getContext(), R.color.bottom_navigation_colors)); 26 | } 27 | 28 | @Override 29 | public void tint() { 30 | setItemIconTintList(ThemeUtils.getThemeColorStateList(getContext(), R.color.bottom_navigation_colors)); 31 | setItemTextColor(ThemeUtils.getThemeColorStateList(getContext(), R.color.bottom_navigation_colors)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/res/anim/enter_from_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/enter_from_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/exit_to_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/exit_to_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/fade_in_long.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/color/bottom_navigation_colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/color/selector_switch_thumb.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/selector_switch_track.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/card_background.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/drawable-hdpi/card_background.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_av_pause_grey_24dp_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/drawable-hdpi/ic_av_pause_grey_24dp_img.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_av_play_arrow_grey_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/drawable-hdpi/ic_av_play_arrow_grey_img.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_clear_grey_24dp_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/drawable-hdpi/ic_clear_grey_24dp_img.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_continue_read_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/drawable-hdpi/ic_continue_read_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_done_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/drawable-hdpi/ic_done_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_start_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/drawable-hdpi/ic_start_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_av_pause_grey_24dp_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/drawable-xhdpi/ic_av_pause_grey_24dp_img.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_av_play_arrow_grey_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/drawable-xhdpi/ic_av_play_arrow_grey_img.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_clear_grey_24dp_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/drawable-xhdpi/ic_clear_grey_24dp_img.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_done_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/drawable-xhdpi/ic_done_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bottom_navigation_colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_status_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/empty_drawable_32dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gradient_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_about_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_down_black_32dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_down_white_32dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_up_black_32dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_up_white_32dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_box_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_box_outline_blank_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_box_x_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_chevron_right_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_chevron_right_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done_all_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_expand_more_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_expand_more_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_extension_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_border_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_download_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_filter_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_import_contacts_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launch_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_palette_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_skip_next_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_skip_previous_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_widgets_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/item_selector_light.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_item_selector_light.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_chapters_selection.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 13 | 14 | 27 | 28 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_extension.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 12 | 13 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 14 | 15 | 20 | 21 | 22 | 30 | 31 | 32 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_result.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 12 | 13 | 17 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 15 | 16 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_task.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 15 | 16 | 22 | 23 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/custom_dialog_preivew.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/custom_drawer_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/custom_progress_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/custom_reader_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 23 | 31 | 38 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/layout/custom_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_download_queue.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_grid.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 22 | 23 | 24 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_source.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_chapter.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_chapter_loadmore.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 22 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_navigation_checkbox.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_navigation_checkdtext.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_navigation_group.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 23 | 24 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_navigation_spinner.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_navigation_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_progress.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_source.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 15 | 23 | 24 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_spinner_common.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 29 | 30 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/menu/bar_menu.xml: -------------------------------------------------------------------------------- 1 | 2 |

4 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/filter_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_bottom_nav.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_grid.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_task.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/menu/reader.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #1a1919 5 | #000000 6 | #88000000 7 | 8 | #eaeaea 9 | #c9c2c2 10 | #2d2d2d 11 | 12 | #504f4f 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/array.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @string/pref_reader_mode_default 5 | @string/pref_reader_mode_page 6 | @string/pref_reader_mode_stream 7 | 8 | 9 | 0 10 | 1 11 | 2 12 | 13 | 14 | @string/default_viewer 15 | @string/left_to_right_viewer 16 | @string/right_to_left_viewer 17 | @string/webtoon_viewer 18 | 19 | 20 | @string/favorite 21 | @string/drawer_history 22 | @string/download 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 24dp 5 | 72dp 6 | 71dp 7 | 56dp 8 | 8dp 9 | 16dp 10 | -------------------------------------------------------------------------------- /app/src/main/res/xml/pref_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/test/java/top/rechinx/meow/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package top.rechinx.meow 2 | 3 | import org.json.JSONArray 4 | import org.junit.Test 5 | 6 | import org.w3c.dom.Element 7 | import org.w3c.dom.Node 8 | import top.rechinx.meow.engine.Helper 9 | import java.io.* 10 | import java.nio.charset.Charset 11 | import java.util.HashMap 12 | 13 | /** 14 | * Example local unit test, which will execute on the development machine (host). 15 | * 16 | * See [testing documentation](http://d.android.com/tools/testing). 17 | */ 18 | class ExampleUnitTest { 19 | 20 | fun readToString(file: File): String? { 21 | val encoding = "UTF-8" 22 | val filelength = file.length() 23 | val filecontent = ByteArray(filelength.toInt()) 24 | try { 25 | val `in` = FileInputStream(file) 26 | `in`.read(filecontent) 27 | `in`.close() 28 | } catch (e: FileNotFoundException) { 29 | e.printStackTrace() 30 | } catch (e: IOException) { 31 | e.printStackTrace() 32 | } 33 | 34 | try { 35 | return String(filecontent, Charset.forName(encoding)) 36 | } catch (e: UnsupportedEncodingException) { 37 | System.err.println("The OS does not support $encoding") 38 | e.printStackTrace() 39 | return null 40 | } 41 | } 42 | 43 | fun print(str: String) = System.out.println(str) 44 | 45 | @Test 46 | fun testEngine() { 47 | // val file = File("/Users/chin/workshop/Meow/app/src/test/java/top/rechinx/meow/test.xml") 48 | // //System.out.println(readToString(file)) 49 | // var root = Helper.getXmlRoot(readToString(file)!!) 50 | // var head = root.getElementsByTagName("head").item(0) 51 | // var body = root.getElementsByTagName("body").item(0) as Element 52 | // var bodyList = HashMap() 53 | // for(i in 0 until head.childNodes.length) { 54 | // val node = head.childNodes.item(i) 55 | // if(node.nodeType == Node.ELEMENT_NODE) { 56 | // if(node.firstChild.nodeType == Node.TEXT_NODE) { 57 | // print(node.nodeName + " " + node.firstChild.nodeValue) 58 | // } 59 | // } 60 | // } 61 | 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/test/java/top/rechinx/meow/test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DMZJ 5 | intro 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /art/01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/art/01.jpg -------------------------------------------------------------------------------- /art/02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/art/02.jpg -------------------------------------------------------------------------------- /art/03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/art/03.jpg -------------------------------------------------------------------------------- /art/04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/art/04.jpg -------------------------------------------------------------------------------- /art/05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/art/05.jpg -------------------------------------------------------------------------------- /art/06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/art/06.jpg -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.10' 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.2.1' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | maven { url "https://jitpack.io" } 23 | maven { url "https://dl.bintray.com/drummer-aidan/maven/" } 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | android.enableJetifier=true 10 | android.useAndroidX=true 11 | org.gradle.jvmargs=-Xmx1536m 12 | # When configured, Gradle will run in incubating parallel mode. 13 | # This option should only be used with decoupled projects. More details, visit 14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 15 | # org.gradle.parallel=true 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mabDc/Meow/baf92de4365e38ca68fec615eb57beaa35c9f45e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Oct 14 12:01:12 CST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/DirectionalViewPager/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/DirectionalViewPager/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 27 5 | 6 | defaultConfig { 7 | minSdkVersion 14 8 | targetSdkVersion 27 9 | versionCode 1 10 | versionName "1.0" 11 | } 12 | 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | } 17 | } 18 | } 19 | 20 | dependencies { 21 | compileOnly 'androidx.legacy:legacy-support-core-ui:1.0.0-rc01' 22 | } 23 | -------------------------------------------------------------------------------- /library/DirectionalViewPager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /library/DirectionalViewPager/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':library:DirectionalViewPager', ':library:Rikka:rikka' 2 | --------------------------------------------------------------------------------