├── app ├── .gitignore ├── src │ ├── main │ │ ├── ic_launcher-web.png │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── attrs.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── drawable │ │ │ │ ├── side_nav_bar.xml │ │ │ │ ├── ic_home.xml │ │ │ │ ├── ic_info.xml │ │ │ │ ├── ic_open_in_browser.xml │ │ │ │ ├── ic_refresh.xml │ │ │ │ ├── ic_search.xml │ │ │ │ ├── ic_book.xml │ │ │ │ └── ic_settings.xml │ │ │ ├── menu │ │ │ │ ├── main.xml │ │ │ │ ├── menu_comic_info.xml │ │ │ │ ├── activity_main_drawer.xml │ │ │ │ ├── menu_main.xml │ │ │ │ └── menu_detail.xml │ │ │ ├── layout │ │ │ │ ├── content_comic_detail.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── comic_issue_item.xml │ │ │ │ ├── content_main.xml │ │ │ │ ├── nav_header_main.xml │ │ │ │ ├── site_list_item.xml │ │ │ │ ├── comic_page_item.xml │ │ │ │ ├── app_bar_main.xml │ │ │ │ ├── comic_page_item_loading.xml │ │ │ │ ├── activity_comic_detail.xml │ │ │ │ ├── comic_list_item.xml │ │ │ │ └── activity_comic_page.xml │ │ │ └── values-v21 │ │ │ │ └── styles.xml │ │ ├── java │ │ │ └── cc │ │ │ │ └── aoeiuv020 │ │ │ │ └── comic │ │ │ │ ├── di │ │ │ │ ├── ext.kt │ │ │ │ ├── site.kt │ │ │ │ ├── genre.kt │ │ │ │ ├── search.kt │ │ │ │ ├── page.kt │ │ │ │ ├── detail.kt │ │ │ │ ├── app.kt │ │ │ │ └── list.kt │ │ │ │ ├── presenter │ │ │ │ ├── ext.kt │ │ │ │ ├── detail.kt │ │ │ │ ├── page.kt │ │ │ │ ├── list.kt │ │ │ │ └── main.kt │ │ │ │ ├── App.kt │ │ │ │ ├── api │ │ │ │ ├── ext.kt │ │ │ │ ├── data.kt │ │ │ │ ├── ComicContext.kt │ │ │ │ ├── popomh.kt │ │ │ │ ├── manhuatai.kt │ │ │ │ └── dm5.kt │ │ │ │ └── ui │ │ │ │ ├── base │ │ │ │ ├── MainBaseNavigationActivity.kt │ │ │ │ └── ComicPageBaseFullScreenActivity.kt │ │ │ │ ├── widget │ │ │ │ └── ScrollAwareFABBehavior.java │ │ │ │ ├── ext.kt │ │ │ │ ├── detail.kt │ │ │ │ ├── list.kt │ │ │ │ ├── main.kt │ │ │ │ └── page.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── cc │ │ │ └── aoeiuv020 │ │ │ └── comic │ │ │ ├── ExampleUnitTest.kt │ │ │ ├── api │ │ │ ├── PopomhContextTest.kt │ │ │ ├── Dm5ContextTest.kt │ │ │ └── ManhuataiContextTest.kt │ │ │ └── di │ │ │ └── daggerTest.kt │ └── androidTest │ │ └── java │ │ └── cc │ │ └── aoeiuv020 │ │ └── comic │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── screenshots ├── detail.jpg ├── genre.jpg ├── image.jpg └── search.jpg ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── .gitignore ├── README.md ├── LICENSE ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /screenshots/detail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/screenshots/detail.jpg -------------------------------------------------------------------------------- /screenshots/genre.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/screenshots/genre.jpg -------------------------------------------------------------------------------- /screenshots/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/screenshots/image.jpg -------------------------------------------------------------------------------- /screenshots/search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/screenshots/search.jpg -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AoEiuV020/PaComic/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #F4FF0E 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Sep 09 18:59:32 CST 2017 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-4.1-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/di/ext.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.di 2 | 3 | import cc.aoeiuv020.comic.api.ComicContext 4 | 5 | /** 6 | * 定义拓展, 7 | * Created by AoEiuV020 on 2017.09.12-17:53:32. 8 | */ 9 | 10 | internal fun ctx(url: String): ComicContext = ComicContext.getComicContext(url)!! -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | #66000000 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/side_nav_bar.xml: -------------------------------------------------------------------------------- 1 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 8dp 6 | 176dp 7 | 16dp 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/test/java/cc/aoeiuv020/comic/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_comic_info.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_open_in_browser.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/presenter/ext.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package cc.aoeiuv020.comic.presenter 4 | 5 | import android.content.Context 6 | 7 | /** 8 | * 拓展,主要对view层的拓展, 9 | * 为了在presenter层方便使用, 10 | * Created by AoEiuV020 on 2017.09.18-17:03:13. 11 | */ 12 | 13 | /** 14 | * 封装一些Context通用的方法, 15 | */ 16 | interface ContextView { 17 | val ctx: Context 18 | } 19 | 20 | fun ContextView.str(id: Int): String = ctx.getString(id) 21 | fun ContextView.str(id: Int, vararg formatArgs: String): String = ctx.getString(id, *formatArgs) 22 | -------------------------------------------------------------------------------- /app/src/main/res/menu/activity_main_drawer.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_comic_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/di/site.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.di 2 | 3 | import cc.aoeiuv020.comic.api.ComicContext 4 | import cc.aoeiuv020.comic.api.ComicSite 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.Subcomponent 8 | import io.reactivex.Observable 9 | 10 | /** 11 | * 提供网站信息, 12 | * Created by AoEiuV020 on 2017.09.12-14:15:55. 13 | */ 14 | @Subcomponent(modules = arrayOf(SiteModule::class)) 15 | interface SiteComponent { 16 | fun getSites(): Observable> 17 | } 18 | 19 | @Module 20 | class SiteModule { 21 | @Provides 22 | fun getSites(): Observable> = Observable.fromCallable { 23 | ComicContext.getComicContexts().map(ComicContext::getComicSite) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/di/genre.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.di 2 | 3 | import cc.aoeiuv020.comic.api.ComicGenre 4 | import cc.aoeiuv020.comic.api.ComicSite 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.Subcomponent 8 | import io.reactivex.Observable 9 | 10 | /** 11 | * 提供漫画分类信息, 12 | * Created by AoEiuV020 on 2017.09.12-15:21:09. 13 | */ 14 | @Subcomponent(modules = arrayOf(GenreModule::class)) 15 | interface GenreComponent { 16 | fun getGenres(): Observable 17 | } 18 | 19 | @Module 20 | class GenreModule(private val site: ComicSite) { 21 | @Provides 22 | fun getGenres(): Observable = Observable.fromCallable { 23 | ctx(site.baseUrl).getGenres() 24 | }.flatMapIterable { it } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/di/search.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.di 2 | 3 | import cc.aoeiuv020.comic.api.ComicGenre 4 | import cc.aoeiuv020.comic.api.ComicSite 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.Subcomponent 8 | import io.reactivex.Observable 9 | 10 | /** 11 | * 提供漫画搜索结果, 12 | * Created by AoEiuV020 on 2017.09.17-16:53:14. 13 | */ 14 | @Subcomponent(modules = arrayOf(SearchModule::class)) 15 | interface SearchComponent { 16 | fun search(): Observable 17 | } 18 | 19 | @Module 20 | class SearchModule(private val site: ComicSite, private var name: String) { 21 | @Provides 22 | fun search(): Observable = Observable.fromCallable { 23 | ctx(site.baseUrl).search(name) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/di/page.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.di 2 | 3 | import cc.aoeiuv020.comic.api.ComicIssue 4 | import cc.aoeiuv020.comic.api.ComicPage 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.Subcomponent 8 | import io.reactivex.Observable 9 | 10 | /** 11 | * 提供漫画图片页面, 12 | * Created by AoEiuV020 on 2017.09.12-18:09:59. 13 | */ 14 | @Subcomponent(modules = arrayOf(PageModule::class)) 15 | interface PageComponent { 16 | fun getComicPages(): Observable> 17 | } 18 | 19 | @Module 20 | class PageModule(private val comicIssue: ComicIssue) { 21 | @Provides 22 | fun getComicPages(): Observable> = Observable.fromCallable { 23 | ctx(comicIssue.url).getComicPages(comicIssue) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/cc/aoeiuv020/comic/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getTargetContext() 20 | assertEquals("cc.aoeiuv020.comic", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/di/detail.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.di 2 | 3 | import cc.aoeiuv020.comic.api.ComicDetail 4 | import cc.aoeiuv020.comic.api.ComicDetailUrl 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.Subcomponent 8 | import io.reactivex.Observable 9 | 10 | /** 11 | * 提供漫画详情, 12 | * Created by AoEiuV020 on 2017.09.12-18:09:48. 13 | */ 14 | @Subcomponent(modules = arrayOf(DetailModule::class)) 15 | interface DetailComponent { 16 | fun getComicDetail(): Observable 17 | } 18 | 19 | @Module 20 | class DetailModule(private val comicDetailUrl: ComicDetailUrl) { 21 | @Provides 22 | fun getComicDetail(): Observable = Observable.fromCallable { 23 | ctx(comicDetailUrl.url).getComicDetail(comicDetailUrl) 24 | } 25 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | #org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/App.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import cc.aoeiuv020.comic.di.AppComponent 6 | import cc.aoeiuv020.comic.di.AppModule 7 | import cc.aoeiuv020.comic.di.DaggerAppComponent 8 | 9 | /** 10 | * 在这里初始化一些东西, 11 | * Created by AoEiuV020 on 2017.09.13-18:27:20. 12 | */ 13 | class App : Application() { 14 | companion object { 15 | lateinit var component: AppComponent 16 | fun setComponent(ctx: Context) { 17 | component = DaggerAppComponent.builder() 18 | .appModule(AppModule(ctx)) 19 | .build() 20 | } 21 | } 22 | 23 | override fun onCreate() { 24 | super.onCreate() 25 | 26 | setComponent(this) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/di/app.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.di 2 | 3 | import android.content.Context 4 | import dagger.Component 5 | import dagger.Module 6 | import dagger.Provides 7 | 8 | /** 9 | * 提供全局context, 10 | * Created by AoEiuV020 on 2017.09.13-18:28:38. 11 | */ 12 | @Component(modules = arrayOf(AppModule::class)) 13 | interface AppComponent { 14 | val ctx: Context 15 | 16 | fun plus(module: SiteModule): SiteComponent 17 | fun plus(module: GenreModule): GenreComponent 18 | fun plus(module: ListModule): ListComponent 19 | fun plus(module: DetailModule): DetailComponent 20 | fun plus(module: PageModule): PageComponent 21 | fun plus(module: SearchModule): SearchComponent 22 | } 23 | 24 | @Module 25 | class AppModule(val ctx: Context) { 26 | @Provides 27 | fun ctx() = ctx 28 | } -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 12 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 13 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Android template 2 | # Built application files 3 | *.apk 4 | *.ap_ 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # Intellij 37 | *.iml 38 | .idea/ 39 | 40 | # Keystore files 41 | *.jks 42 | 43 | # External native build folder generated in Android Studio 2.2 and later 44 | .externalNativeBuild 45 | 46 | # Google Services (e.g. APIs or Firebase) 47 | google-services.json 48 | 49 | # Freeline 50 | freeline.py 51 | freeline/ 52 | freeline_project_description.json 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/di/list.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.di 2 | 3 | import cc.aoeiuv020.comic.api.ComicGenre 4 | import cc.aoeiuv020.comic.api.ComicListItem 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.Subcomponent 8 | import io.reactivex.Observable 9 | 10 | /** 11 | * 提供漫画列表, 12 | * Created by AoEiuV020 on 2017.09.12-17:49:22. 13 | */ 14 | @Subcomponent(modules = arrayOf(ListModule::class)) 15 | interface ListComponent { 16 | fun getComicList(): Observable> 17 | fun getNextPage(): Observable 18 | } 19 | 20 | @Module 21 | class ListModule(private val comicGenre: ComicGenre) { 22 | @Provides 23 | fun getComicList(): Observable> = Observable.fromCallable { 24 | ctx(comicGenre.url).getComicList(comicGenre) 25 | } 26 | 27 | @Provides 28 | fun getNextPage(): Observable = Observable.create { em -> 29 | ctx(comicGenre.url).getNextPage(comicGenre)?.let { 30 | em.onNext(it) 31 | } 32 | em.onComplete() 33 | } 34 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 爬漫画 2 | 3 | 停止维护,可能合并到爬小说PaNovel, 4 | 5 | ## 应用简介 6 | 突然意识到版权问题,正失去梦想成为咸鱼, 7 | 本项目仅供开发者学习使用,侵删, 8 | 不上线,不宣传,万一人多了也只能删了, 9 | 10 | 目前支持三个网站,侵删, 11 | [漫画台](http://www.manhuatai.com) 12 | [动漫屋](http://www.dm5.com) 13 | [泡泡漫画](http://www.popomh.com) 14 | 15 | 【背景】 16 | 市面上的漫画软件都是都是从特定的某个漫画网站下载漫画, 17 | 有的漫画在这个网站没有,而有这漫画的网站可能没有相应的app, 18 | 只能用浏览器看,体验很糟糕, 19 | 20 | 【理想】 21 | 一个类几个方法就分析出漫画网站的一切, 22 | 然后爬出漫画, 23 | 计划未来要支持大多数漫画网站, 24 | 25 | 【库】 26 | kotlin + mvp + dagger2 + rxandroid 27 | [jsoup](https://github.com/jhy/jsoup) 28 | [dagger2](https://github.com/google/dagger) 29 | [RxAndroid](https://github.com/ReactiveX/RxAndroid) 30 | [glide](https://github.com/bumptech/glide) 31 | [anko](https://github.com/Kotlin/anko) 32 | [MaterialSearchView](https://github.com/MiguelCatalan/MaterialSearchView) 33 | [PinchImageView](https://github.com/boycy815/PinchImageView) 34 | [slf4j](https://github.com/qos-ch/slf4j) 35 | 36 | ![img](screenshots/genre.jpg) 37 | ![img](screenshots/search.jpg) 38 | ![img](screenshots/detail.jpg) 39 | ![img](screenshots/image.jpg) 40 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_book.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 啊o额iu鱼 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/comic_issue_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/presenter/detail.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.presenter 2 | 3 | import cc.aoeiuv020.comic.App 4 | import cc.aoeiuv020.comic.api.ComicListItem 5 | import cc.aoeiuv020.comic.di.DetailModule 6 | import cc.aoeiuv020.comic.ui.ComicDetailActivity 7 | import cc.aoeiuv020.comic.ui.async 8 | import org.jetbrains.anko.AnkoLogger 9 | import org.jetbrains.anko.error 10 | 11 | /** 12 | * 管理详情页的界面和数据, 13 | * 启动时请求漫画详情页的信息, 14 | * 然后就没了, 15 | * Created by AoEiuV020 on 2017.09.18-17:52:06. 16 | */ 17 | class ComicDetailPresenter(private val view: ComicDetailActivity, private val comicListItem: ComicListItem) : AnkoLogger { 18 | fun start() { 19 | requestComicDetail() 20 | } 21 | 22 | private fun requestComicDetail() { 23 | App.component.plus(DetailModule(comicListItem.detailUrl)) 24 | .getComicDetail() 25 | .async() 26 | .subscribe({ comicDetail -> 27 | view.showComicDetail(comicDetail) 28 | }, { e -> 29 | val message = "加载漫画详情失败," 30 | error(message, e) 31 | view.showError(message, e) 32 | }) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/api/ext.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package cc.aoeiuv020.comic.api 4 | 5 | import org.slf4j.Logger 6 | 7 | /** 8 | * 定义slf4j一系列拓展, 9 | * 主要就是先判断再执行lambda, 10 | * Created by AoEiuV020 on 2017.09.17-11:37:41. 11 | */ 12 | 13 | inline fun Logger.trace(message: () -> Any?) { 14 | if (isTraceEnabled) { 15 | trace("{}", message().toString()) 16 | } 17 | } 18 | 19 | inline fun Logger.debug(message: () -> Any?) { 20 | if (isDebugEnabled) { 21 | debug("{}", message().toString()) 22 | } 23 | } 24 | 25 | inline fun Logger.info(message: () -> Any?) { 26 | if (isInfoEnabled) { 27 | info("{}", message().toString()) 28 | } 29 | } 30 | 31 | inline fun Logger.warn(message: () -> Any?) { 32 | if (isWarnEnabled) { 33 | warn("{}", message().toString()) 34 | } 35 | } 36 | 37 | inline fun Logger.error(message: () -> Any?) { 38 | if (isErrorEnabled) { 39 | error("{}", message().toString()) 40 | } 41 | } 42 | 43 | inline fun Logger.error(e: Throwable, message: () -> Any?) { 44 | if (isErrorEnabled) { 45 | error(message().toString(), e) 46 | } 47 | } 48 | 49 | fun String.pick(pattern: String) = replace(Regex(pattern), "$1") -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/nav_header_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 22 | 23 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 爬漫画 3 | 4 | 设置 5 | 加载中..%s 6 | 漫画详情页 7 | 网站 8 | 9 | Open navigation drawer 10 | Close navigation drawer 11 | 12 | ComicPageActivity 13 | Dummy Button 14 | DUMMY\nCONTENT 15 | 漫画列表 16 | 分类列表 17 | 漫画详情 18 | 漫画页面 19 | 已经是最后一页了 20 | 下一页 21 | %d/%d 22 | 浏览 23 | 简介 24 | 搜索结果 25 | 浏览失败或者不支持该漫画 26 | 没有上一话了 27 | 没有下一话了 28 | 正在加载上一话 29 | 正在加载下一话 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 | 27 | 28 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/site_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/comic_page_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 18 | 19 | 26 | 27 | 28 | 31 | 32 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/app_bar_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 18 | 19 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/comic_page_item_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 24 | 25 | 38 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/ui/base/MainBaseNavigationActivity.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.ui.base 2 | 3 | import android.os.Bundle 4 | import android.support.design.widget.NavigationView 5 | import android.support.v4.view.GravityCompat 6 | import android.support.v7.app.ActionBarDrawerToggle 7 | import android.support.v7.app.AppCompatActivity 8 | import cc.aoeiuv020.comic.R 9 | import kotlinx.android.synthetic.main.activity_main.* 10 | import kotlinx.android.synthetic.main.app_bar_main.* 11 | 12 | /** 13 | * 抽屉Activity,绝大部分代码是自动生成的, 14 | * 分离出来仅供activity_main使用, 15 | * Created by AoEiuV020 on 2017.09.18-20:32:06. 16 | */ 17 | @Suppress("MemberVisibilityCanPrivate", "unused") 18 | abstract class MainBaseNavigationActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(R.layout.activity_main) 23 | setSupportActionBar(toolbar) 24 | 25 | val toggle = ActionBarDrawerToggle( 26 | this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) 27 | drawer_layout.addDrawerListener(toggle) 28 | toggle.syncState() 29 | 30 | nav_view.setNavigationItemSelectedListener(this) 31 | } 32 | 33 | fun isDrawerOpen() = drawer_layout.isDrawerOpen(GravityCompat.START) 34 | 35 | fun closeDrawer() { 36 | drawer_layout.closeDrawer(GravityCompat.START) 37 | } 38 | 39 | fun openDrawer() { 40 | drawer_layout.openDrawer(GravityCompat.START) 41 | } 42 | 43 | override fun onBackPressed() { 44 | if (isDrawerOpen()) { 45 | closeDrawer() 46 | } else { 47 | if (searchView.isSearchOpen) { 48 | searchView.closeSearch() 49 | } else { 50 | super.onBackPressed() 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/ui/widget/ScrollAwareFABBehavior.java: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.ui.widget; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.design.widget.CoordinatorLayout; 6 | import android.support.design.widget.FloatingActionButton; 7 | import android.support.v4.view.ViewCompat; 8 | import android.util.AttributeSet; 9 | import android.view.View; 10 | 11 | /** 12 | * fab消失显示的行为, 13 | * Created by AoEiuV020 on 2017.09.24-21:29:42. 14 | */ 15 | @SuppressWarnings("ALL") 16 | public class ScrollAwareFABBehavior extends CoordinatorLayout.Behavior { 17 | public ScrollAwareFABBehavior(Context context, AttributeSet attrs) { 18 | super(); 19 | } 20 | 21 | @Override 22 | public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, 23 | @NonNull FloatingActionButton child, @NonNull View directTargetChild, @NonNull View target, 24 | @ViewCompat.ScrollAxis int axes, @ViewCompat.NestedScrollType int type) { 25 | return true; 26 | } 27 | 28 | @Override 29 | public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull final FloatingActionButton child, 30 | @NonNull View target, int dxConsumed, int dyConsumed, 31 | int dxUnconsumed, int dyUnconsumed, @ViewCompat.NestedScrollType int type) { 32 | super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type); 33 | if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) { 34 | // 传个回调进去,设为INVISIBLE就不会被跳过了,解决下拉时fab不会重新显示的问题, 35 | child.hide(new FloatingActionButton.OnVisibilityChangedListener() { 36 | @Override 37 | public void onHidden(FloatingActionButton fab) { 38 | child.setVisibility(View.INVISIBLE); 39 | } 40 | }); 41 | } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) { 42 | child.show(); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | #-keep class com.bumptech.glide.integration.okhttp3.OkHttpGlideModule 23 | 24 | -keepclassmembers class * implements java.io.Serializable { 25 | static final long serialVersionUID; 26 | private static final java.io.ObjectStreamField[] serialPersistentFields; 27 | !static !transient ; 28 | private void writeObject(java.io.ObjectOutputStream); 29 | private void readObject(java.io.ObjectInputStream); 30 | java.lang.Object writeReplace(); 31 | java.lang.Object readResolve(); 32 | } 33 | 34 | 35 | 36 | #glide https://github.com/bumptech/glide#proguard 37 | -keep public class * implements com.bumptech.glide.module.GlideModule 38 | -keep public class * extends com.bumptech.glide.AppGlideModule 39 | -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { 40 | **[] $VALUES; 41 | public *; 42 | } 43 | # for DexGuard only 44 | #-keepresourcexmlelements manifest/application/meta-data@value=GlideModule 45 | 46 | 47 | 48 | #jsoup https://stackoverflow.com/a/32169975/5615186 49 | -keeppackagenames org.jsoup.nodes 50 | 51 | 52 | #apk 包内所有 class 的内部结构 53 | -dump class_files.txt 54 | #未混淆的类和成员 55 | -printseeds seeds.txt 56 | #列出从 apk 中删除的代码 57 | -printusage unused.txt 58 | #混淆前后的映射 59 | -printmapping mapping.txt 60 | #各种问题通通无视 61 | -dontusemixedcaseclassnames 62 | -dontskipnonpubliclibraryclasses 63 | -verbose 64 | -ignorewarnings 65 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/presenter/page.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.presenter 2 | 3 | import cc.aoeiuv020.comic.App 4 | import cc.aoeiuv020.comic.api.ComicDetailUrl 5 | import cc.aoeiuv020.comic.api.ComicIssue 6 | import cc.aoeiuv020.comic.di.DetailModule 7 | import cc.aoeiuv020.comic.di.PageModule 8 | import cc.aoeiuv020.comic.ui.ComicPageActivity 9 | import cc.aoeiuv020.comic.ui.async 10 | import org.jetbrains.anko.AnkoLogger 11 | import org.jetbrains.anko.debug 12 | import org.jetbrains.anko.error 13 | 14 | /** 15 | * 管理漫画图片页的界面和数据, 16 | * 初始化时传入漫画地址用来得到章节列表, 17 | * 启动时加载当前章节漫画并显示, 18 | * 另外还有加载上一话和下一话的处理, 19 | * Created by AoEiuV020 on 2017.09.18-18:24:20. 20 | */ 21 | class ComicPagePresenter(private val view: ComicPageActivity, val url: String, private var index: Int) : AnkoLogger { 22 | private lateinit var issueAsc: List 23 | fun start() { 24 | App.component.plus(DetailModule(ComicDetailUrl(url))).getComicDetail() 25 | .async().subscribe({ comicDetail -> 26 | issueAsc = comicDetail.issuesAsc 27 | requestComicPages(false) 28 | }, { e -> 29 | val message = "加载漫画章节列表失败," 30 | error(message, e) 31 | view.showError(message, e) 32 | }) 33 | } 34 | 35 | private fun requestComicPages(previous: Boolean) { 36 | val issue = issueAsc[index] 37 | debug { "请求${if (previous) "上" else "下"}一话($index.${issue.name})全图片地址" } 38 | App.component.plus(PageModule(issue)) 39 | .getComicPages() 40 | .async() 41 | .subscribe({ pages -> 42 | if (previous) 43 | view.showPreviousIssue(issue, pages) 44 | else 45 | view.showNextIssue(issue, pages) 46 | }, { e -> 47 | val message = "加载漫画页面失败," 48 | error(message, e) 49 | view.showError(message, e) 50 | }) 51 | } 52 | 53 | fun requestPreviousIssue() { 54 | if (index == 0) { 55 | view.showNoPreviousIssue() 56 | return 57 | } 58 | --index 59 | requestComicPages(true) 60 | } 61 | 62 | fun requestNextIssue() { 63 | if (index == issueAsc.size - 1) { 64 | view.showNoNextIssue() 65 | return 66 | } 67 | ++index 68 | requestComicPages(false) 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/ui/ext.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused", "DEPRECATION") 2 | 3 | package cc.aoeiuv020.comic.ui 4 | 5 | import android.app.Activity 6 | import android.app.ProgressDialog 7 | import android.content.Context 8 | import android.support.v7.app.AlertDialog 9 | import android.view.View 10 | import android.widget.ImageView 11 | import cc.aoeiuv020.comic.R 12 | import com.bumptech.glide.Glide 13 | import com.bumptech.glide.RequestBuilder 14 | import com.bumptech.glide.RequestManager 15 | import com.bumptech.glide.request.RequestOptions 16 | import io.reactivex.Observable 17 | import io.reactivex.android.schedulers.AndroidSchedulers 18 | import io.reactivex.schedulers.Schedulers 19 | import org.jetbrains.anko.alert 20 | 21 | /** 22 | * 拓展, 23 | * Created by AoEiuV020 on 2017.09.12-18:33:43. 24 | */ 25 | 26 | fun Observable.async(): Observable = this 27 | .subscribeOn(Schedulers.io()) 28 | .observeOn(AndroidSchedulers.mainThread()) 29 | 30 | fun asyncLoadImage(image: ImageView, url: String) { 31 | Glide.with(image).load(url).into(image) 32 | } 33 | 34 | /** 35 | * 下载过程保持原来的图片, 36 | */ 37 | fun RequestBuilder.holdInto(image: ImageView) 38 | = apply(RequestOptions().placeholder(image.drawable)).into(image) 39 | 40 | fun View.hide() { 41 | visibility = View.GONE 42 | } 43 | 44 | fun View.show() { 45 | visibility = View.VISIBLE 46 | } 47 | 48 | fun Context.alertError(message: String, e: Throwable) = alert(message + "\n" + e.message).show() 49 | 50 | /** 51 | * 如果Activity已经销毁了,返回null, 52 | * 要是直接调用Glide.with,会报 53 | */ 54 | fun Activity.glide(): RequestManager? = if (isDestroyed) null else Glide.with(this) 55 | 56 | fun Activity.glide(callback: (RequestManager) -> Unit) = glide()?.also { callback(it) } 57 | 58 | fun Context.loading(dialog: ProgressDialog, id: Int) = loading(dialog, getString(R.string.loading, getString(id))) 59 | fun Context.loading(dialog: ProgressDialog, str: String) = dialog.apply { 60 | setMessage(str) 61 | show() 62 | } 63 | 64 | fun Context.alertError(dialog: AlertDialog, str: String, e: Throwable) = alert(dialog, str + "\n" + e.message) 65 | fun Context.alert(dialog: AlertDialog, messageId: Int) = alert(dialog, getString(messageId)) 66 | fun Context.alert(dialog: AlertDialog, messageId: Int, titleId: Int) = alert(dialog, getString(messageId), getString(titleId)) 67 | fun Context.alert(dialog: AlertDialog, message: String, title: String? = null) = dialog.apply { 68 | dialog.setMessage(message) 69 | title?.let { 70 | dialog.setTitle(title) 71 | } 72 | show() 73 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_comic_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 25 | 26 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/comic_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 15 | 27 | 28 | 41 | 42 | 55 | 56 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | apply plugin: 'kotlin-kapt' 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion compile_version 9 | buildToolsVersion build_version 10 | defaultConfig { 11 | applicationId "cc.aoeiuv020.comic" 12 | minSdkVersion min_version 13 | targetSdkVersion target_version 14 | versionCode 6 15 | versionName "0.6" 16 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 17 | setProperty("archivesBaseName", "comic-$versionName") 18 | vectorDrawables.useSupportLibrary = true 19 | } 20 | buildTypes { 21 | release { 22 | minifyEnabled true 23 | shrinkResources true 24 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility JavaVersion.VERSION_1_7 29 | targetCompatibility JavaVersion.VERSION_1_7 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation "com.android.support:appcompat-v7:$support_version" 35 | implementation "com.android.support.constraint:constraint-layout:$constraint_layout_version" 36 | implementation "com.android.support:design:$support_version" 37 | implementation "com.android.support:cardview-v7:$support_version" 38 | implementation "com.android.support:recyclerview-v7:$support_version" 39 | implementation "com.android.support:support-v4:$support_version" 40 | testImplementation "junit:junit:$junit_version" 41 | androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.1', { 42 | exclude group: 'com.android.support', module: 'support-annotations' 43 | }) 44 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" 45 | implementation "org.slf4j:slf4j-api:$slf4j_version" 46 | testImplementation "org.slf4j:slf4j-simple:$slf4j_version" 47 | debugRuntimeOnly "org.slf4j:slf4j-android:$slf4j_version" 48 | implementation "org.jsoup:jsoup:$jsoup_version" 49 | implementation 'com.google.dagger:dagger:2.11' 50 | kapt 'com.google.dagger:dagger-compiler:2.11' 51 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' 52 | implementation 'io.reactivex.rxjava2:rxjava:2.1.3' 53 | implementation "org.jetbrains.anko:anko-commons:$anko_version" 54 | implementation "com.github.bumptech.glide:glide:$glide_version" 55 | kapt "com.github.bumptech.glide:compiler:$glide_version" 56 | implementation 'com.miguelcatalan:materialsearchview:1.4.0' 57 | testImplementation 'org.mockito:mockito-core:2.8.47' 58 | implementation 'com.github.AoEiuV020:PinchImageView:1.2@aar' 59 | implementation 'com.google.code.gson:gson:2.8.1' 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/presenter/list.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.presenter 2 | 3 | import android.content.Context 4 | import cc.aoeiuv020.comic.App 5 | import cc.aoeiuv020.comic.api.ComicGenre 6 | import cc.aoeiuv020.comic.di.ListComponent 7 | import cc.aoeiuv020.comic.di.ListModule 8 | import cc.aoeiuv020.comic.ui.ComicListFragment 9 | import cc.aoeiuv020.comic.ui.async 10 | import org.jetbrains.anko.AnkoLogger 11 | import org.jetbrains.anko.debug 12 | import org.jetbrains.anko.error 13 | 14 | /** 15 | * 管理漫画列表界面和数据, 16 | * Created by AoEiuV020 on 2017.09.23-22:47:15. 17 | */ 18 | class ComicListPresenter(private val view: ComicListFragment) : AnkoLogger { 19 | private var listComponent: ListComponent? = null 20 | 21 | fun requestComicList(genre: ComicGenre) { 22 | saveGenre(genre) 23 | App.component.plus(ListModule(genre)).also { listComponent = it } 24 | .getComicList() 25 | .async() 26 | .subscribe({ comicList -> 27 | view.showComicList(comicList) 28 | }, { e -> 29 | val message = "加载漫画列表失败," 30 | error(message, e) 31 | view.showError(message, e) 32 | }) 33 | } 34 | 35 | private fun saveGenre(genre: ComicGenre) { 36 | App.component.ctx.getSharedPreferences("genre", Context.MODE_PRIVATE) 37 | .edit() 38 | .putString("name", genre.name) 39 | .putString("url", genre.url) 40 | .apply() 41 | } 42 | 43 | fun loadNextPage() { 44 | debug { "加载下一页,已经设置listComponent: ${listComponent != null}" } 45 | listComponent?.run { 46 | getNextPage().async().toList().subscribe({ genres -> 47 | if (genres.isEmpty()) { 48 | debug { "没有下一页" } 49 | view.showYetLastPage() 50 | return@subscribe 51 | } 52 | val genre = genres.first() 53 | view.showUrl(genre.url) 54 | App.component.plus(ListModule(genre)).also { listComponent = it } 55 | .getComicList() 56 | .async() 57 | .subscribe({ comicList -> 58 | debug { "展示漫画列表,数量:${comicList.size}" } 59 | view.addComicList(comicList) 60 | }, { e -> 61 | val message = "加载下一页漫画列表失败," 62 | error(message, e) 63 | view.showError(message, e) 64 | }) 65 | }, { e -> 66 | val message = "加载漫画列表一下页地址失败," 67 | error(message, e) 68 | view.showError(message, e) 69 | }) 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/test/java/cc/aoeiuv020/comic/api/PopomhContextTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.api 2 | 3 | import junit.framework.Assert.assertEquals 4 | import org.junit.Before 5 | import org.junit.Test 6 | 7 | /** 8 | * 泡泡漫画网的测试类, 9 | * Created by AoEiuV020 on 2017.09.09-22:17:08. 10 | */ 11 | class PopomhContextTest { 12 | init { 13 | System.setProperty("org.slf4j.simpleLogger.log.PopomhContext", "info") 14 | } 15 | 16 | private lateinit var context: PopomhContext 17 | @Before 18 | fun setUp() { 19 | context = PopomhContext() 20 | } 21 | 22 | @Test 23 | fun getGenres() { 24 | context.getGenres().forEach { 25 | println("[${it.name}](${it.url})") 26 | } 27 | } 28 | 29 | @Test 30 | fun getNextPage() { 31 | val genreList = listOf("http://www.popomh.com/comic/", 32 | "http://www.popomh.com/comic/10.html", 33 | "http://www.popomh.com/comic/1091.html", 34 | "http://www.popomh.com/comic/class_4.html", 35 | "http://www.popomh.com/comic/class_4/22.html", 36 | "http://www.popomh.com/comic/class_4/29.html", 37 | "http://www.popomh.com/comic/?act=search&st=%E6%9F%AF%E5%8D%97") 38 | .map { ComicGenre("", it) } 39 | genreList.forEach { 40 | context.getNextPage(it).let { 41 | println(it?.url) 42 | } 43 | } 44 | } 45 | 46 | @Test 47 | fun getComicList() { 48 | val genreList = listOf("http://www.popomh.com/comic/class_8.html", 49 | "http://www.popomh.com/comic/?act=search&st=%E6%9F%AF%E5%8D%97") 50 | .map { ComicGenre("", it) } 51 | genreList.forEach { 52 | context.getComicList(it).forEach { 53 | println(it.name) 54 | println(it.detailUrl) 55 | println(it.img) 56 | } 57 | } 58 | } 59 | 60 | @Test 61 | fun search() { 62 | context.search("柯南").let { 63 | println(it.name) 64 | println(it.url) 65 | } 66 | } 67 | 68 | @Test 69 | fun getComicDetail() { 70 | context.getComicDetail(ComicDetailUrl("http://www.popomh.com/manhua/32551.html")).let { 71 | assertEquals("狩猎史莱姆300年", it.name) 72 | it.bigImg.subscribe { 73 | println(it) 74 | } 75 | println(it.info) 76 | it.issuesAsc.forEach { 77 | println("[${it.name}](${it.url})") 78 | } 79 | } 80 | } 81 | 82 | @Test 83 | fun getComicPages() { 84 | val imgList = listOf("http://124.94201314.net/dm03//ok-comic03/A/32744/act_008/z_0001_20239.PNG", 85 | "http://124.94201314.net/dm03//ok-comic03/A/32744/act_008/z_0002_20503.JPG") 86 | context.getComicPages(ComicIssue("", "http://www.popomh.com/popo290338/1.html?s=3")).forEachIndexed { index, (url) -> 87 | url.subscribe { (img) -> 88 | assertEquals(imgList[index], img) 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/api/data.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package cc.aoeiuv020.comic.api 4 | 5 | import io.reactivex.Observable 6 | import java.io.Serializable 7 | 8 | /** 9 | * 相关的数据类定义在这文件里, 10 | * Created by AoEiuV020 on 2017.09.12-14:35:04. 11 | */ 12 | 13 | /** 14 | * 基类, 15 | */ 16 | open class Data : Serializable 17 | 18 | /** 19 | * 漫画网站信息, 20 | */ 21 | data class ComicSite( 22 | val name: String, 23 | val baseUrl: String, 24 | val logo: String 25 | ) : Data() 26 | 27 | /** 28 | * 封装漫画详情页地址, 29 | */ 30 | data class ComicDetailUrl( 31 | val url: String 32 | ) : Data() 33 | 34 | /** 35 | * 漫画分类页面, 36 | * 该分类第一页地址, 37 | */ 38 | data class ComicGenre( 39 | val name: String, 40 | val url: String 41 | ) : Data() 42 | 43 | /** 44 | * 漫画列表中的一个漫画, 45 | * @param detailUrl 该漫画详情页地址, 46 | */ 47 | data class ComicListItem( 48 | val name: String, 49 | val img: Observable, 50 | val detailUrl: ComicDetailUrl, 51 | val info: String = "" 52 | ) : Data() { 53 | constructor(name: String, img: Observable, url: String, info: String = "") 54 | : this(name, img, ComicDetailUrl(url), info) 55 | 56 | constructor(name: String, img: ComicImage, url: String, info: String = "") 57 | : this(name, Observable.just(img), url, info) 58 | 59 | constructor(name: String, img: String, url: String, info: String = "") 60 | : this(name, ComicImage(img), url, info) 61 | 62 | constructor(name: String, img: ComicImage, url: ComicDetailUrl, info: String = "") 63 | : this(name, Observable.just(img), url, info) 64 | 65 | constructor(name: String, img: String, url: ComicDetailUrl, info: String = "") 66 | : this(name, ComicImage(img), url, info) 67 | } 68 | 69 | /** 70 | * 漫画详情页, 71 | * @param issuesAsc 升序章节, 72 | */ 73 | data class ComicDetail( 74 | val name: String, 75 | val bigImg: Observable, 76 | val info: String, 77 | val issuesAsc: List 78 | ) : Data() { 79 | constructor(name: String, bigImg: ComicImage, info: String, issuesAsc: List) : 80 | this(name, Observable.just(bigImg), info, issuesAsc) 81 | 82 | constructor(name: String, bigImg: String, info: String, issuesAsc: List) : 83 | this(name, ComicImage(bigImg), info, issuesAsc) 84 | } 85 | 86 | /** 87 | * 漫画目录, 88 | * @param url 本章节第一页地址, 89 | */ 90 | data class ComicIssue( 91 | /** 92 | * 章节名不包括漫画名, 93 | */ 94 | val name: String, 95 | val url: String 96 | ) : Data() 97 | 98 | /** 99 | * 漫画页面, 100 | */ 101 | data class ComicPage( 102 | val img: Observable 103 | ) : Data() { 104 | constructor(comicImage: ComicImage) : this(Observable.just(comicImage)) 105 | constructor(s: String) : this(ComicImage(s)) 106 | } 107 | 108 | /** 109 | * 漫画图片, 110 | * @param realUrl 漫画图片地址, 111 | * @param cacheableUrl 漫画图片地址, 112 | */ 113 | data class ComicImage( 114 | val realUrl: String, 115 | val cacheableUrl: String 116 | ) : Data() { 117 | constructor(img: String) : this(img, img) 118 | } 119 | -------------------------------------------------------------------------------- /app/src/test/java/cc/aoeiuv020/comic/di/daggerTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.di 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import cc.aoeiuv020.comic.App 6 | import cc.aoeiuv020.comic.api.ComicDetailUrl 7 | import cc.aoeiuv020.comic.api.ComicGenre 8 | import cc.aoeiuv020.comic.api.ComicIssue 9 | import cc.aoeiuv020.comic.api.ComicSite 10 | import org.junit.Before 11 | import org.junit.Test 12 | import org.mockito.Mockito 13 | 14 | /** 15 | * 测试dagger, 16 | * Created by AoEiuV020 on 2017.09.12-17:55:42. 17 | */ 18 | 19 | class DaggerTest { 20 | @Before 21 | fun setUp() { 22 | val sp = Mockito.mock(SharedPreferences::class.java) 23 | val spe = Mockito.mock(SharedPreferences.Editor::class.java) 24 | val ctx = Mockito.mock(Context::class.java) 25 | Mockito.`when`(ctx.getSharedPreferences(Mockito.anyString(), Mockito.anyInt())).thenReturn(sp) 26 | Mockito.`when`(sp.edit()).thenReturn(spe) 27 | Mockito.`when`(spe.putString(Mockito.anyString(), Mockito.anyString())).thenReturn(spe) 28 | App.setComponent(ctx) 29 | } 30 | 31 | @Test 32 | fun getSites() { 33 | val siteComponent: SiteComponent = App.component.plus(SiteModule()) 34 | siteComponent.getSites().flatMapIterable { it } 35 | .forEach { 36 | println(it.name) 37 | println(it.baseUrl) 38 | println(it.logo) 39 | } 40 | } 41 | 42 | @Test 43 | fun getGenres() { 44 | val genreComponent: GenreComponent = App.component.plus(GenreModule(ComicSite("", "http://www.popomh.com", ""))) 45 | genreComponent.getGenres() 46 | .forEach { 47 | println(it.name) 48 | println(it.url) 49 | } 50 | } 51 | 52 | @Test 53 | fun getComicList() { 54 | val listComponent: ListComponent = App.component.plus(ListModule(ComicGenre("", "http://www.popomh.com/comic/class_8.html"))) 55 | listComponent.getComicList() 56 | .flatMapIterable { it } 57 | .forEach { 58 | println(it.name) 59 | println(it.detailUrl) 60 | println(it.img) 61 | } 62 | } 63 | 64 | @Test 65 | fun getComicDetail() { 66 | val detailComponent: DetailComponent = App.component.plus(DetailModule(ComicDetailUrl("http://www.popomh.com/manhua/32551.html"))) 67 | detailComponent.getComicDetail() 68 | .forEach { 69 | println(it.name) 70 | it.bigImg.subscribe { 71 | println(it) 72 | } 73 | println(it.info) 74 | it.issuesAsc.forEach { 75 | println("[${it.name}](${it.url})") 76 | } 77 | } 78 | } 79 | 80 | @Test 81 | fun getComicPages() { 82 | val pageComponent: PageComponent = App.component.plus(PageModule(ComicIssue("", "http://www.popomh.com/popo290025/1.html?s=3"))) 83 | pageComponent.getComicPages().flatMapIterable { it } 84 | .forEach { 85 | it.img.subscribe { 86 | println(it) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/presenter/main.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.presenter 2 | 3 | import android.content.Context 4 | import cc.aoeiuv020.comic.App 5 | import cc.aoeiuv020.comic.api.ComicContext 6 | import cc.aoeiuv020.comic.api.ComicGenre 7 | import cc.aoeiuv020.comic.api.ComicSite 8 | import cc.aoeiuv020.comic.di.GenreModule 9 | import cc.aoeiuv020.comic.di.SearchModule 10 | import cc.aoeiuv020.comic.di.SiteModule 11 | import cc.aoeiuv020.comic.ui.MainActivity 12 | import cc.aoeiuv020.comic.ui.async 13 | import org.jetbrains.anko.AnkoLogger 14 | import org.jetbrains.anko.debug 15 | import org.jetbrains.anko.error 16 | 17 | /** 18 | * 管理主页的界面和数据, 19 | * 启动时读取之前的选择,网站和分类, 20 | * Created by AoEiuV020 on 2017.09.18-15:30:37. 21 | */ 22 | class MainPresenter(private val view: MainActivity) : AnkoLogger { 23 | fun start() { 24 | debug { "读取记住的选择," } 25 | loadSite()?.also { site -> 26 | debug { "已有记住网站:${site.name}" } 27 | view.showSite(site) 28 | loadGenre(site)?.let { genre -> 29 | debug { "已有记住分类:${genre.name}" } 30 | view.showGenre(genre) 31 | } ?: run { 32 | debug { "没有记住的分类," } 33 | } 34 | } ?: run { 35 | debug { "没有记住的网站,弹出网站选择," } 36 | requestSites() 37 | } 38 | } 39 | 40 | /** 41 | * 提供记住了的分类选择, 42 | */ 43 | private fun loadGenre(site: ComicSite): ComicGenre? { 44 | return App.component.ctx.getSharedPreferences("genre", Context.MODE_PRIVATE).run { 45 | val url = getString("url", null) ?: return null 46 | val name = getString("name", "") 47 | // 仅当url属于这个site, 48 | ComicGenre(name, url).takeIf { ComicContext.getComicContext(site.baseUrl)!!.check(url) } 49 | } 50 | } 51 | 52 | /** 53 | * 保存记住了的网站选择, 54 | */ 55 | private fun saveSite(site: ComicSite) { 56 | App.component.ctx.getSharedPreferences("site", Context.MODE_PRIVATE) 57 | .edit() 58 | .putString("baseUrl", site.baseUrl) 59 | .apply() 60 | } 61 | 62 | /** 63 | * 提供记住了的网站选择, 64 | */ 65 | private fun loadSite(): ComicSite? { 66 | val baseUrl = App.component.ctx.getSharedPreferences("site", Context.MODE_PRIVATE) 67 | .getString("baseUrl", "") 68 | return ComicContext.getComicContext(baseUrl)?.getComicSite() 69 | } 70 | 71 | fun requestSites() { 72 | App.component.plus(SiteModule()) 73 | .getSites() 74 | .async() 75 | .subscribe { sites -> 76 | view.showSites(sites) 77 | } 78 | } 79 | 80 | fun search(site: ComicSite, query: String) { 81 | debug { "在网站(${site.name})搜索:$query, " } 82 | App.component.plus(SearchModule(site, query)).search().async().subscribe({ genre -> 83 | view.showGenre(genre) 84 | }, { e -> 85 | val message = "加载搜索结果失败," 86 | error(message, e) 87 | view.showError(message, e) 88 | }) 89 | } 90 | 91 | fun requestGenres(site: ComicSite) { 92 | saveSite(site) 93 | App.component.plus(GenreModule(site)) 94 | .getGenres() 95 | .async() 96 | .toList() 97 | .subscribe({ genres -> 98 | debug { "加载网站分类列表成功,数量:${genres.size}" } 99 | view.showGenres(genres) 100 | }, { e -> 101 | val message = "加载网站分类列表失败," 102 | error(message, e) 103 | view.showError(message, e) 104 | }) 105 | } 106 | } -------------------------------------------------------------------------------- /app/src/test/java/cc/aoeiuv020/comic/api/Dm5ContextTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.api 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Assert.assertTrue 5 | import org.junit.Before 6 | import org.junit.Test 7 | 8 | /** 9 | * 动漫屋的测试类, 10 | * Created by AoEiuV020 on 2017.09.13-16:29:56. 11 | */ 12 | class Dm5ContextTest { 13 | init { 14 | System.setProperty("org.slf4j.simpleLogger.log.Dm5Context", "info") 15 | } 16 | 17 | private lateinit var context: Dm5Context 18 | @Before 19 | fun setUp() { 20 | context = Dm5Context() 21 | } 22 | 23 | @Test 24 | fun getGenres() { 25 | context.getGenres().forEach { 26 | println("[${it.name}](${it.url})") 27 | } 28 | } 29 | 30 | @Test 31 | fun getNextPage() { 32 | val genreList = listOf("http://www.dm5.com/manhua-shaonianrexue/", 33 | "http://www.dm5.com/manhua-shaonianrexue-p2/", 34 | "http://www.dm5.com/manhua-shaonianrexue-p149/", 35 | "http://www.dm5.com/search?title=%E6%9F%AF%E5%8D%97&language=1") 36 | .map { ComicGenre("", it) } 37 | genreList.forEach { 38 | context.getNextPage(it).let { 39 | println(it?.url) 40 | } 41 | } 42 | } 43 | 44 | @Test 45 | fun getComicList() { 46 | val genreList = listOf("http://www.dm5.com/manhua-shaonianrexue/", 47 | "http://www.dm5.com/search?title=%E6%9F%AF%E5%8D%97&language=1", 48 | "http://www.dm5.com/manhua-latest/") 49 | .map { ComicGenre("", it) } 50 | genreList.forEach { 51 | context.getComicList(it).forEach { 52 | println(it.name) 53 | println(it.detailUrl) 54 | println(it.img) 55 | println(it.info) 56 | } 57 | } 58 | } 59 | 60 | @Test 61 | fun search() { 62 | context.search("柯南").let { 63 | println(it.name) 64 | println(it.url) 65 | } 66 | } 67 | 68 | @Test 69 | fun getComicDetail() { 70 | context.getComicDetail(ComicDetailUrl("http://www.dm5.com/manhua-yaojingdeweiba/")).let { 71 | assertEquals("妖精的尾巴", it.name) 72 | it.bigImg.subscribe { 73 | println(it) 74 | } 75 | println(it.info) 76 | it.issuesAsc.forEach { 77 | println("[${it.name}](${it.url})") 78 | } 79 | } 80 | } 81 | 82 | @Test 83 | fun getComicPages() { 84 | val imgList = listOf("http://manhua1032-61-174-50-99.cdndm5.com/34/33771/523824/1_1219.jpg?cid=523824", 85 | "http://manhua1032-61-174-50-99.cdndm5.com/34/33771/523824/2_2667.jpg?cid=523824", 86 | "http://manhua1032-61-174-50-99.cdndm5.com/34/33771/523824/3_8727.jpg?cid=523824", 87 | "http://manhua1032-61-174-50-99.cdndm5.com/34/33771/523824/4_1681.jpg?cid=523824", 88 | "http://manhua1032-61-174-50-99.cdndm5.com/34/33771/523824/5_1055.jpg?cid=523824", 89 | "http://manhua1032-61-174-50-99.cdndm5.com/34/33771/523824/6_5063.jpg?cid=523824", 90 | "http://manhua1032-61-174-50-99.cdndm5.com/34/33771/523824/7_8270.jpg?cid=523824", 91 | "http://manhua1032-61-174-50-99.cdndm5.com/34/33771/523824/8_3589.jpg?cid=523824") 92 | context.getComicPages(ComicIssue("", "http://www.dm5.com/m523824/")).forEachIndexed { index, (url) -> 93 | url.subscribe { (img, cacheableUrl) -> 94 | assertEquals(imgList[index], cacheableUrl) 95 | assertTrue(img.startsWith(imgList[index])) 96 | } 97 | } 98 | /* 99 | assertEquals("http://manhua1032-61-174-50-99.cdndm5.com/34/33771/523824/1_1219.jpg?cid=523824", it.cacheableUrl) 100 | assertTrue(it.img.startsWith("http://manhua1032-61-174-50-99.cdndm5.com/34/33771/523824/1_1219.jpg?cid=523824")) 101 | */ 102 | } 103 | } -------------------------------------------------------------------------------- /app/src/test/java/cc/aoeiuv020/comic/api/ManhuataiContextTest.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.api 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Before 5 | import org.junit.Test 6 | 7 | /** 8 | * 漫画台测试类, 9 | * Created by AoEiuV020 on 2017.09.19-11:19:01. 10 | */ 11 | class ManhuataiContextTest { 12 | init { 13 | System.setProperty("org.slf4j.simpleLogger.log.ManhuataiContext", "info") 14 | } 15 | 16 | private lateinit var context: ManhuataiContext 17 | @Before 18 | fun setUp() { 19 | context = ManhuataiContext() 20 | } 21 | 22 | @Test 23 | fun getGenres() { 24 | context.getGenres().forEach { 25 | println("[${it.name}](${it.url})") 26 | } 27 | } 28 | 29 | @Test 30 | fun getNextPage() { 31 | val genreList = listOf("http://www.manhuatai.com/zhiyinmanke.html", 32 | "http://www.manhuatai.com/getjson.shtml?d=1505801840099&q=%E6%9F%AF%E5%8D%97", 33 | "http://www.manhuatai.com/all.html", 34 | "http://www.manhuatai.com/all_p813.html") 35 | .map { ComicGenre("", it) } 36 | genreList.forEach { 37 | context.getNextPage(it).let { 38 | println(it?.url) 39 | } 40 | } 41 | } 42 | 43 | @Test 44 | fun getComicList() { 45 | val genreList = listOf("http://www.manhuatai.com/zhiyinmanke.html", 46 | "http://www.manhuatai.com/getjson.shtml?d=1505801840099&q=%E6%9F%AF%E5%8D%97", 47 | "http://www.manhuatai.com/all.html") 48 | .map { ComicGenre("", it) } 49 | genreList.forEach { 50 | context.getComicList(it).forEach { 51 | println(it.name) 52 | println(it.detailUrl) 53 | it.img.subscribe { 54 | println(it) 55 | } 56 | println(it.info) 57 | } 58 | } 59 | } 60 | 61 | @Test 62 | fun search() { 63 | context.search("柯南").let { 64 | println(it.name) 65 | println(it.url) 66 | } 67 | } 68 | 69 | @Test 70 | fun getComicDetail() { 71 | context.getComicDetail(ComicDetailUrl("http://www.manhuatai.com/doupocangqiong/")).let { 72 | assertEquals("斗破苍穹", it.name) 73 | it.bigImg.subscribe { 74 | println(it) 75 | } 76 | println(it.info) 77 | it.issuesAsc.forEach { 78 | println("[${it.name}](${it.url})") 79 | } 80 | } 81 | } 82 | 83 | @Test 84 | fun getComicPages() { 85 | val imgList = listOf("http://mhpic.zymkcdn.com/comic/D%2F%E6%96%97%E7%A0%B4%E8%8B%8D%E7%A9%B9%E6%8B%86%E5%88%86%E7%89%88%2F615%E8%AF%9D%2F1.jpg-mht.middle", 86 | "http://mhpic.zymkcdn.com/comic/D%2F%E6%96%97%E7%A0%B4%E8%8B%8D%E7%A9%B9%E6%8B%86%E5%88%86%E7%89%88%2F615%E8%AF%9D%2F2.jpg-mht.middle", 87 | "http://mhpic.zymkcdn.com/comic/D%2F%E6%96%97%E7%A0%B4%E8%8B%8D%E7%A9%B9%E6%8B%86%E5%88%86%E7%89%88%2F615%E8%AF%9D%2F3.jpg-mht.middle", 88 | "http://mhpic.zymkcdn.com/comic/D%2F%E6%96%97%E7%A0%B4%E8%8B%8D%E7%A9%B9%E6%8B%86%E5%88%86%E7%89%88%2F615%E8%AF%9D%2F4.jpg-mht.middle", 89 | "http://mhpic.zymkcdn.com/comic/D%2F%E6%96%97%E7%A0%B4%E8%8B%8D%E7%A9%B9%E6%8B%86%E5%88%86%E7%89%88%2F615%E8%AF%9D%2F5.jpg-mht.middle", 90 | "http://mhpic.zymkcdn.com/comic/D%2F%E6%96%97%E7%A0%B4%E8%8B%8D%E7%A9%B9%E6%8B%86%E5%88%86%E7%89%88%2F615%E8%AF%9D%2F6.jpg-mht.middle", 91 | "http://mhpic.zymkcdn.com/comic/D%2F%E6%96%97%E7%A0%B4%E8%8B%8D%E7%A9%B9%E6%8B%86%E5%88%86%E7%89%88%2F615%E8%AF%9D%2F7.jpg-mht.middle", 92 | "http://mhpic.zymkcdn.com/comic/D%2F%E6%96%97%E7%A0%B4%E8%8B%8D%E7%A9%B9%E6%8B%86%E5%88%86%E7%89%88%2F615%E8%AF%9D%2F8.jpg-mht.middle") 93 | context.getComicPages(ComicIssue("", "http://www.manhuatai.com/doupocangqiong/dpcq_615h.html")).forEachIndexed { index, (url) -> 94 | url.subscribe { (img) -> 95 | assertEquals(imgList[index], img) 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_comic_page.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 17 | 18 | 23 | 24 | 29 | 30 | 36 | 37 | 51 | 52 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 70 | 71 | 79 | 80 | 85 | 86 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/api/ComicContext.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.api 2 | 3 | import org.jsoup.Jsoup 4 | import org.jsoup.nodes.Document 5 | import org.jsoup.nodes.Element 6 | import org.jsoup.nodes.Node 7 | import org.jsoup.nodes.TextNode 8 | import org.jsoup.select.Elements 9 | import org.jsoup.select.NodeTraversor 10 | import org.jsoup.select.NodeVisitor 11 | import org.slf4j.Logger 12 | import org.slf4j.LoggerFactory 13 | import java.io.IOException 14 | import java.net.URL 15 | 16 | /** 17 | * 漫画网站上下文, 18 | * 一个Context对象贯穿始终, 19 | * Created by AoEiuV020 on 2017.09.09-20:50:30. 20 | */ 21 | @Suppress("unused") 22 | abstract class ComicContext { 23 | companion object { 24 | @Suppress("RemoveExplicitTypeArguments") 25 | private val contexts = listOf(ManhuataiContext(), Dm5Context(), PopomhContext()) 26 | private val contextsMap = contexts.associateBy { URL(it.getComicSite().baseUrl).host } 27 | fun getComicContexts(): List = contexts 28 | fun getComicContext(url: String): ComicContext? { 29 | val host: String 30 | try { 31 | host = URL(url).host 32 | } catch (_: Exception) { 33 | return null 34 | } 35 | return contextsMap[host] ?: contexts.firstOrNull { it.check(url) } 36 | } 37 | } 38 | 39 | @Suppress("MemberVisibilityCanPrivate") 40 | protected val logger: Logger = LoggerFactory.getLogger(this.javaClass.simpleName) 41 | 42 | abstract fun getComicSite(): ComicSite 43 | /** 44 | * 获取网站分类信息, 45 | */ 46 | abstract fun getGenres(): List 47 | 48 | /** 49 | * 获取分类页面的下一页, 50 | */ 51 | abstract fun getNextPage(genre: ComicGenre): ComicGenre? 52 | 53 | /** 54 | * 获取分类页面里的漫画列表信息, 55 | */ 56 | abstract fun getComicList(genre: ComicGenre): List 57 | 58 | /** 59 | * 搜索漫画得到漫画列表, 60 | */ 61 | abstract fun search(name: String): ComicGenre 62 | 63 | abstract fun isSearchResult(genre: ComicGenre): Boolean 64 | 65 | /** 66 | * 获取漫画详情页信息, 67 | */ 68 | abstract fun getComicDetail(comicDetailUrl: ComicDetailUrl): ComicDetail 69 | 70 | /** 71 | * 获取章节漫画所有页面信息, 72 | */ 73 | abstract fun getComicPages(comicIssue: ComicIssue): List 74 | 75 | internal fun check(url: String): Boolean = URL(getComicSite().baseUrl).host == URL(url).host 76 | 77 | protected fun getHtml(url: String): Document { 78 | logger.trace { 79 | val stack = Thread.currentThread().stackTrace 80 | stack.drop(2).take(6).joinToString("\n", "stack trace\n") { 81 | "\tat ${it.className}.${it.methodName}(${it.fileName}:${it.lineNumber})" 82 | } 83 | } 84 | logger.debug { "get $url" } 85 | val conn = Jsoup.connect(url) 86 | // 网络连接失败直接抛出, 87 | val root = conn.get() 88 | logger.debug { "status code: ${conn.response().statusCode()}" } 89 | logger.debug { "response url: ${conn.response().url()}" } 90 | if (!check(conn.response().url().toString())) { 91 | throw IOException("网络被重定向,检查网络是否可用,") 92 | } 93 | return root 94 | } 95 | 96 | protected fun absUrl(url: String) = getComicSite().baseUrl + url 97 | protected fun text(e: Element): String = e.text() 98 | protected fun text(e: Elements): String = e.text() 99 | protected fun src(img: Element): String = img.attr("src") 100 | protected fun absHref(a: Element): String = a.absUrl("href") 101 | protected fun title(a: Element): String = a.attr("title") 102 | 103 | /** 104 | * br结点转成换行的方法, 105 | * https://stackoverflow.com/a/17989379/5615186 106 | * @param maxDepth 深度,为 1 则不处理子结点, 107 | */ 108 | protected fun textWithNewLine(element: Element, maxDepth: Int): String { 109 | val buffer = StringBuilder() 110 | NodeTraversor(object : NodeVisitor { 111 | internal var isNewline = true 112 | override fun head(node: Node, depth: Int) { 113 | if (depth > maxDepth) { 114 | return 115 | } 116 | if (node is TextNode) { 117 | val text = node.text().replace('\u00A0', ' ').trim { it <= ' ' } 118 | if (!text.isEmpty()) { 119 | buffer.append(text) 120 | isNewline = false 121 | } 122 | } else if (node is Element) { 123 | if (!isNewline) { 124 | if (node.isBlock || node.tagName() == "br") { 125 | buffer.append("\n") 126 | isNewline = true 127 | } 128 | } 129 | } 130 | } 131 | 132 | override fun tail(node: Node, depth: Int) {} 133 | }).traverse(element) 134 | return buffer.toString() 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/api/popomh.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.api 2 | 3 | import io.reactivex.Observable 4 | import java.net.URLEncoder 5 | 6 | /** 7 | * 泡泡漫画, 8 | * Created by AoEiuV020 on 2017.09.09-21:08:02. 9 | */ 10 | @Suppress("UnnecessaryVariable") 11 | class PopomhContext : ComicContext() { 12 | private val site = ComicSite( 13 | name = "泡泡漫画", 14 | baseUrl = "http://www.popomh.com", 15 | logo = "http://www.popomh.com/images/logo.png" 16 | ) 17 | 18 | override fun getComicSite(): ComicSite = site 19 | override fun getGenres(): List { 20 | val root = getHtml(site.baseUrl) 21 | val elements = root.select("#iHBG > div.cHNav > div > span > a, #iHBG > div.cHNav > div a:has(span)") 22 | return elements.map { 23 | val a = it 24 | ComicGenre(text(a), absHref(a)) 25 | } 26 | } 27 | 28 | override fun getNextPage(genre: ComicGenre): ComicGenre? { 29 | if (isSearchResult(genre)) { 30 | // 这网站搜索结果只有一页, 31 | return null 32 | } 33 | val root = getHtml(genre.url) 34 | val a = root.select("#iComicPC1 > span > a:nth-child(3)").first() 35 | val url = absHref(a) 36 | return if (url.isEmpty()) { 37 | return null 38 | } else { 39 | ComicGenre(genre.name, url) 40 | } 41 | } 42 | 43 | override fun getComicList(genre: ComicGenre): List { 44 | val root = getHtml(genre.url) 45 | val elements = root.select("#list > div.cComicList > li > a") 46 | return elements.map { 47 | val a = it 48 | val img = it.select("img").first() 49 | ComicListItem(text(a), src(img), absHref(a)) 50 | } 51 | } 52 | 53 | override fun search(name: String): ComicGenre { 54 | return ComicGenre(name, searchUrl(name)) 55 | /* 56 | val urlEncodedName = URLEncoder.encode(name, "UTF-8") 57 | val root = getHtml(absUrl("/comic/?act=search&st=$urlEncodedName")) 58 | val elements = root.select("#list > div.cComicList > li > a") 59 | return elements.map { 60 | val a = it 61 | val img = it.select("img").first() 62 | ComicListItem(text(a), src(img), absHref(a)) 63 | } 64 | */ 65 | } 66 | 67 | private fun searchUrl(name: String): String { 68 | val urlEncodedName = URLEncoder.encode(name, "UTF-8") 69 | return absUrl("/comic/?act=search&st=$urlEncodedName") 70 | } 71 | 72 | override fun isSearchResult(genre: ComicGenre): Boolean = genre.url.matches(Regex(".*act=search.*")) 73 | 74 | override fun getComicDetail(comicDetailUrl: ComicDetailUrl): ComicDetail { 75 | val root = getHtml(comicDetailUrl.url) 76 | val name = text(root.select("#about_kit > ul > li:nth-child(1) > h1")).trim() 77 | val img = root.select("#about_style > img").first() 78 | val bigImg = src(img) 79 | val info = root.select("#about_kit > ul > li:not(:nth-child(1))") 80 | .joinToString("\n") { text(it) } 81 | val issues = root.select("#permalink > div.cVolList > ul > li > a").map { 82 | val a = it 83 | ComicIssue(text(a).removePrefix(name), absHref(a)) 84 | }.asReversed() 85 | return ComicDetail(name, bigImg, info, issues) 86 | } 87 | 88 | override fun getComicPages(comicIssue: ComicIssue): List { 89 | val first = getHtml(comicIssue.url) 90 | val pagesCount = first.select("body > div.cHeader > div.cH1 > b") 91 | .first().text().split('/')[1].trim().toInt() 92 | return List(pagesCount) { index -> 93 | ComicPage(Observable.fromCallable { 94 | getComicImage(comicIssue.url.replace(Regex("/\\d*.html"), "/${index + 1}.html")) 95 | }) 96 | } 97 | } 98 | 99 | /** 100 | * 网站有两个图片服务器,默认第一个, 101 | */ 102 | private var domainIndex: Int = 0 103 | 104 | fun getComicImage(url: String): ComicImage { 105 | val root = getHtml(url) 106 | val domains = root.select("#hdDomain").first().attr("value").split('|') 107 | val domain = domains[if (domainIndex < 0) 0 else if (domainIndex > domains.size) domains.size else domainIndex] 108 | val cipher = root.select("#img7652, #img1021, #img2391, #imgCurr") 109 | .first().attr("name") 110 | val img = domain + unsuan(cipher) 111 | return ComicImage(img) 112 | } 113 | 114 | /** 115 | * 从js方法翻译过来, 116 | * 方法名意义未知, 117 | * http://www.popomh.com/script/view.js 118 | */ 119 | private fun unsuan(cipher: String): String { 120 | var s = cipher 121 | val x = s.substring(s.length - 1) 122 | val w = "abcdefghijklmnopqrstuvwxyz" 123 | val xi = w.indexOf(x) + 1 124 | val sk = s.substring(s.length - xi - 12, s.length - xi - 1) 125 | s = s.substring(0, s.length - xi - 12) 126 | val k = sk.substring(0, sk.length - 1) 127 | val f = sk.substring(sk.length - 1) 128 | var i = 0 129 | while (i < k.length) { 130 | s = s.replace(k[i], i.toString()[0]) 131 | i++ 132 | } 133 | val ss = s.split(f) 134 | s = "" 135 | i = 0 136 | while (i < ss.size) { 137 | s += ss[i].toInt().toChar() 138 | i++ 139 | } 140 | return s 141 | } 142 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/ui/detail.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package cc.aoeiuv020.comic.ui 4 | 5 | import android.app.ProgressDialog 6 | import android.content.Context 7 | import android.os.Bundle 8 | import android.support.v4.view.ViewCompat 9 | import android.support.v7.app.AlertDialog 10 | import android.support.v7.app.AppCompatActivity 11 | import android.support.v7.widget.GridLayoutManager 12 | import android.support.v7.widget.RecyclerView 13 | import android.view.LayoutInflater 14 | import android.view.Menu 15 | import android.view.View 16 | import android.view.ViewGroup 17 | import cc.aoeiuv020.comic.R 18 | import cc.aoeiuv020.comic.api.ComicDetail 19 | import cc.aoeiuv020.comic.api.ComicIssue 20 | import cc.aoeiuv020.comic.api.ComicListItem 21 | import cc.aoeiuv020.comic.presenter.ComicDetailPresenter 22 | import kotlinx.android.synthetic.main.activity_comic_detail.* 23 | import kotlinx.android.synthetic.main.activity_comic_detail.view.* 24 | import kotlinx.android.synthetic.main.comic_issue_item.view.* 25 | import kotlinx.android.synthetic.main.content_comic_detail.* 26 | import org.jetbrains.anko.AnkoLogger 27 | import org.jetbrains.anko.browse 28 | import org.jetbrains.anko.debug 29 | import org.jetbrains.anko.startActivity 30 | 31 | class ComicDetailActivity : AppCompatActivity(), AnkoLogger { 32 | private val alertDialog: AlertDialog by lazy { AlertDialog.Builder(this).create() } 33 | private val progressDialog: ProgressDialog by lazy { ProgressDialog(this) } 34 | private lateinit var comicUrl: String 35 | private lateinit var presenter: ComicDetailPresenter 36 | private var comicDetail: ComicDetail? = null 37 | 38 | override fun onCreate(savedInstanceState: Bundle?) { 39 | super.onCreate(savedInstanceState) 40 | setContentView(R.layout.activity_comic_detail) 41 | setSupportActionBar(toolbar) 42 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 43 | toolbar.setNavigationOnClickListener { onBackPressed() } 44 | 45 | comicUrl = intent.getStringExtra("comicUrl") 46 | val comicName = intent.getStringExtra("comicName") 47 | val comicIcon: String? = intent.getStringExtra("comicIcon") 48 | debug { "receive <$comicUrl, $comicName, $comicIcon>" } 49 | val comicListItem = ComicListItem(comicName, comicIcon ?: "", comicUrl) 50 | 51 | comicIcon.let { url -> 52 | glide { 53 | it.load(url).into(image) 54 | } 55 | } 56 | ViewCompat.setTransitionName(image, "image") 57 | 58 | recyclerView.adapter = ComicDetailAdapter(this@ComicDetailActivity) { index -> 59 | startActivity("comicName" to comicName, "comicUrl" to comicUrl, "issueIndex" to index) 60 | } 61 | recyclerView.layoutManager = GridLayoutManager(this@ComicDetailActivity, 3) 62 | 63 | loading(progressDialog, R.string.comic_detail) 64 | toolbar_layout.title = comicName 65 | 66 | fab.setOnClickListener { 67 | startActivity("comicName" to comicName, "comicUrl" to comicUrl) 68 | } 69 | 70 | presenter = ComicDetailPresenter(this, comicListItem) 71 | presenter.start() 72 | } 73 | 74 | fun showComicDetail(detail: ComicDetail) { 75 | this.comicDetail = detail 76 | progressDialog.dismiss() 77 | toolbar_layout.title = detail.name 78 | detail.bigImg.async().subscribe { (img) -> 79 | glide { 80 | it.load(img).holdInto(toolbar_layout.image) 81 | } 82 | } 83 | (recyclerView.adapter as ComicDetailAdapter).setDetail(detail) 84 | } 85 | 86 | fun showError(message: String, e: Throwable) { 87 | progressDialog.dismiss() 88 | alertError(message, e) 89 | } 90 | 91 | private fun showComicAbout() { 92 | comicDetail?.let { 93 | alert(alertDialog, it.info, it.name) 94 | } 95 | } 96 | 97 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 98 | menuInflater.inflate(R.menu.menu_detail, menu) 99 | menu.findItem(R.id.browse).setOnMenuItemClickListener { 100 | browse(comicUrl) 101 | } 102 | menu.findItem(R.id.info).setOnMenuItemClickListener { 103 | showComicAbout() 104 | true 105 | } 106 | return true 107 | } 108 | } 109 | 110 | class ComicDetailAdapter(val ctx: Context, val callback: (Int) -> Unit) : RecyclerView.Adapter() { 111 | private lateinit var detail: ComicDetail 112 | private var issuesDesc = emptyList() 113 | override fun getItemCount() = issuesDesc.size 114 | 115 | override fun onBindViewHolder(holder: Holder, position: Int) { 116 | val issue = issuesDesc[position] 117 | holder.root.apply { 118 | name.text = issue.name 119 | } 120 | } 121 | 122 | override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int) 123 | = Holder(LayoutInflater.from(ctx).inflate(R.layout.comic_issue_item, parent, false)) 124 | 125 | fun setDetail(detail: ComicDetail) { 126 | this.detail = detail 127 | issuesDesc = detail.issuesAsc.asReversed() 128 | notifyDataSetChanged() 129 | } 130 | 131 | inner class Holder(val root: View) : RecyclerView.ViewHolder(root), AnkoLogger { 132 | init { 133 | root.setOnClickListener { 134 | // 传出话数的顺序索引, 135 | callback(issuesDesc.size - 1 - layoutPosition) 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle requestComicDetail up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to requestComicDetail the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM'str JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/ui/list.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package cc.aoeiuv020.comic.ui 4 | 5 | import android.app.Activity 6 | import android.app.ProgressDialog 7 | import android.os.Bundle 8 | import android.support.v4.app.ActivityOptionsCompat 9 | import android.support.v4.app.Fragment 10 | import android.support.v7.app.AlertDialog 11 | import android.view.LayoutInflater 12 | import android.view.View 13 | import android.view.ViewGroup 14 | import android.widget.AbsListView 15 | import android.widget.BaseAdapter 16 | import cc.aoeiuv020.comic.R 17 | import cc.aoeiuv020.comic.api.ComicGenre 18 | import cc.aoeiuv020.comic.api.ComicListItem 19 | import cc.aoeiuv020.comic.presenter.ComicListPresenter 20 | import kotlinx.android.synthetic.main.comic_list_item.view.* 21 | import kotlinx.android.synthetic.main.content_main.* 22 | import org.jetbrains.anko.AnkoLogger 23 | import org.jetbrains.anko.intentFor 24 | 25 | /** 26 | * 展示漫画列表, 27 | * Created by AoEiuV020 on 2017.09.23-22:38:56. 28 | */ 29 | class ComicListFragment : Fragment() { 30 | private val alertDialog: AlertDialog by lazy { AlertDialog.Builder(context).create() } 31 | private val progressDialog: ProgressDialog by lazy { ProgressDialog(context) } 32 | private lateinit var presenter: ComicListPresenter 33 | private var isEnd = false 34 | private var isLoadingNextPage = false 35 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 36 | val root = inflater.inflate(R.layout.content_main, container, false) 37 | presenter = ComicListPresenter(this) 38 | return root 39 | } 40 | 41 | fun showError(message: String, e: Throwable) { 42 | progressDialog.dismiss() 43 | context.alertError(alertDialog, message, e) 44 | } 45 | 46 | fun showComicList(comicList: List) { 47 | isLoadingNextPage = false 48 | progressDialog.dismiss() 49 | listView.run { 50 | adapter = ComicListAdapter(activity, comicList) 51 | setOnItemClickListener { _, view, position, _ -> 52 | val item = adapter.getItem(position) as ComicListItem 53 | val intent = context.intentFor("comicUrl" to item.detailUrl.url, 54 | "comicName" to item.name, 55 | "comicIcon" to view.comic_icon.getTag(R.id.comic_icon)) 56 | val options = ActivityOptionsCompat 57 | .makeSceneTransitionAnimation(activity, view.comic_icon, "image") 58 | startActivity(intent, options.toBundle()) 59 | } 60 | setOnScrollListener(object : AbsListView.OnScrollListener { 61 | private var lastItem = 0 62 | 63 | override fun onScroll(view: AbsListView?, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) { 64 | // 求画面上最后一个的索引,并不准,可能是最后一个+1, 65 | lastItem = firstVisibleItem + visibleItemCount 66 | } 67 | 68 | override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) { 69 | // 差不多就好,反正没到底也快了, 70 | if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE 71 | && lastItem >= adapter.count - 2) { 72 | if (isLoadingNextPage || isEnd) { 73 | return 74 | } 75 | isLoadingNextPage = true 76 | context.loading(progressDialog, R.string.next_page) 77 | presenter.loadNextPage() 78 | } 79 | } 80 | }) 81 | } 82 | } 83 | 84 | fun addComicList(comicList: List) { 85 | isLoadingNextPage = false 86 | progressDialog.dismiss() 87 | if (listView.adapter != null) { 88 | (listView.adapter as ComicListAdapter).addAll(comicList) 89 | } else { 90 | showComicList(comicList) 91 | } 92 | } 93 | 94 | fun showYetLastPage() { 95 | isEnd = true 96 | isLoadingNextPage = false 97 | progressDialog.dismiss() 98 | context.alert(alertDialog, R.string.yet_last_page) 99 | } 100 | 101 | fun showGenre(genre: ComicGenre) { 102 | isEnd = false 103 | isLoadingNextPage = false 104 | context.loading(progressDialog, R.string.comic_list) 105 | presenter.requestComicList(genre) 106 | } 107 | 108 | fun showUrl(url: String) { 109 | (activity as MainActivity).showUrl(url) 110 | } 111 | } 112 | 113 | class ComicListAdapter(val ctx: Activity, data: List) : BaseAdapter(), AnkoLogger { 114 | private val items = data.toMutableList() 115 | override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View 116 | = (convertView ?: View.inflate(ctx, R.layout.comic_list_item, null)).apply { 117 | val comic = getItem(position) 118 | comic_name.text = comic.name 119 | comic_info.text = comic.info 120 | comic_icon.setImageDrawable(null) 121 | comic.img.async().subscribe { (img) -> 122 | comic_icon.setTag(R.id.comic_icon, img) 123 | ctx.glide { 124 | it.load(img).into(comic_icon) 125 | } 126 | } 127 | } 128 | 129 | override fun getItem(position: Int) = items[position] 130 | 131 | override fun getItemId(position: Int) = 0L 132 | 133 | override fun getCount() = items.size 134 | fun addAll(comicList: List) { 135 | items.addAll(comicList) 136 | notifyDataSetChanged() 137 | } 138 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/ui/main.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package cc.aoeiuv020.comic.ui 4 | 5 | import android.app.Activity 6 | import android.app.ProgressDialog 7 | import android.os.Bundle 8 | import android.support.design.widget.Snackbar 9 | import android.support.v7.app.AlertDialog 10 | import android.view.Menu 11 | import android.view.MenuItem 12 | import android.view.View 13 | import android.view.ViewGroup 14 | import android.widget.BaseAdapter 15 | import cc.aoeiuv020.comic.R 16 | import cc.aoeiuv020.comic.api.ComicGenre 17 | import cc.aoeiuv020.comic.api.ComicSite 18 | import cc.aoeiuv020.comic.presenter.MainPresenter 19 | import cc.aoeiuv020.comic.ui.base.MainBaseNavigationActivity 20 | import com.miguelcatalan.materialsearchview.MaterialSearchView 21 | import kotlinx.android.synthetic.main.activity_main.* 22 | import kotlinx.android.synthetic.main.app_bar_main.* 23 | import kotlinx.android.synthetic.main.nav_header_main.view.* 24 | import kotlinx.android.synthetic.main.site_list_item.view.* 25 | import org.jetbrains.anko.AnkoLogger 26 | import org.jetbrains.anko.browse 27 | import org.jetbrains.anko.debug 28 | 29 | 30 | /** 31 | * 主页,展示网站,分类, 32 | * Created by AoEiuV020 on 2017.09.12-19:04:44. 33 | */ 34 | 35 | class MainActivity : MainBaseNavigationActivity(), AnkoLogger { 36 | private val alertDialog: AlertDialog by lazy { AlertDialog.Builder(this).create() } 37 | private val progressDialog: ProgressDialog by lazy { ProgressDialog(this) } 38 | private var url: String = "https://github.com/AoEiuV020/comic" 39 | private lateinit var presenter: MainPresenter 40 | private lateinit var genres: List 41 | private var site: ComicSite? = null 42 | 43 | override fun onCreate(savedInstanceState: Bundle?) { 44 | super.onCreate(savedInstanceState) 45 | 46 | searchView.setOnQueryTextListener(object : MaterialSearchView.OnQueryTextListener { 47 | override fun onQueryTextSubmit(query: String): Boolean { 48 | // 收起软键盘, 49 | searchView.hideKeyboard(searchView) 50 | site?.also { 51 | loading(progressDialog, R.string.search_result) 52 | presenter.search(it, query) 53 | } ?: run { 54 | debug { "没有选择网站,先弹出网站选择," } 55 | presenter.requestSites() 56 | } 57 | return true 58 | } 59 | 60 | override fun onQueryTextChange(newText: String?): Boolean = false 61 | 62 | }) 63 | 64 | presenter = MainPresenter(this) 65 | presenter.start() 66 | } 67 | 68 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 69 | menuInflater.inflate(R.menu.menu_main, menu) 70 | val item = menu.findItem(R.id.action_search) 71 | searchView.setMenuItem(item) 72 | menu.findItem(R.id.browse).setOnMenuItemClickListener { 73 | browse(url) 74 | } 75 | return true 76 | } 77 | 78 | private val GROUP_ID: Int = 1 79 | 80 | override fun onNavigationItemSelected(item: MenuItem): Boolean { 81 | // Handle navigation view item clicks here. 82 | when (item.groupId) { 83 | GROUP_ID -> { 84 | showGenre(genres[item.order]) 85 | } 86 | else -> when (item.itemId) { 87 | R.id.select_sites -> presenter.requestSites() 88 | R.id.settings -> { 89 | Snackbar.make(drawer_layout, "没实现", Snackbar.LENGTH_SHORT).show() 90 | } 91 | } 92 | } 93 | closeDrawer() 94 | return true 95 | } 96 | 97 | fun showUrl(url: String) { 98 | this.url = url 99 | } 100 | 101 | fun showGenre(genre: ComicGenre) { 102 | title = genre.name 103 | url = genre.url 104 | progressDialog.dismiss() 105 | (fragment_container as ComicListFragment).showGenre(genre) 106 | closeDrawer() 107 | } 108 | 109 | fun showSites(sites: List) { 110 | AlertDialog.Builder(this@MainActivity).setAdapter(SiteListAdapter(this@MainActivity, sites)) { _, index -> 111 | val site = sites[index] 112 | this.site = site 113 | debug { "选中网站:${site.name},弹出侧栏," } 114 | showSite(site) 115 | }.show() 116 | } 117 | 118 | fun showSite(site: ComicSite) { 119 | this.site = site 120 | url = site.baseUrl 121 | openDrawer() 122 | loading(progressDialog, R.string.genre_list) 123 | nav_view.getHeaderView(0).apply { 124 | selectedSiteName.text = site.name 125 | glide { 126 | it.load(site.logo).holdInto(selectedSiteLogo) 127 | } 128 | } 129 | presenter.requestGenres(site) 130 | } 131 | 132 | fun showGenres(genres: List) { 133 | this.genres = genres 134 | progressDialog.dismiss() 135 | nav_view.menu.run { 136 | removeGroup(GROUP_ID) 137 | genres.forEachIndexed { index, (name) -> 138 | add(GROUP_ID, index, index, name) 139 | } 140 | } 141 | } 142 | 143 | fun showError(message: String, e: Throwable) { 144 | progressDialog.dismiss() 145 | alertError(alertDialog, message, e) 146 | } 147 | } 148 | 149 | class SiteListAdapter(val ctx: Activity, private val sites: List) : BaseAdapter() { 150 | override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { 151 | val view = convertView ?: View.inflate(ctx, R.layout.site_list_item, null) 152 | val site = getItem(position) 153 | view.apply { 154 | siteName.text = site.name 155 | ctx.glide { 156 | it.load(site.logo).into(siteLogo) 157 | } 158 | } 159 | return view 160 | } 161 | 162 | override fun getItem(position: Int) = sites[position] 163 | 164 | override fun getItemId(position: Int) = 0L 165 | 166 | override fun getCount() = sites.size 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/ui/base/ComicPageBaseFullScreenActivity.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.ui.base 2 | 3 | import android.annotation.TargetApi 4 | import android.content.Context 5 | import android.os.Build 6 | import android.os.Bundle 7 | import android.os.Handler 8 | import android.support.v7.app.AppCompatActivity 9 | import android.util.AttributeSet 10 | import android.view.MenuItem 11 | import android.view.View 12 | import android.view.WindowInsets 13 | import android.view.WindowManager 14 | import android.widget.FrameLayout 15 | import cc.aoeiuv020.comic.R 16 | import cc.aoeiuv020.comic.ui.hide 17 | import cc.aoeiuv020.comic.ui.show 18 | import kotlinx.android.synthetic.main.activity_comic_page.* 19 | import org.jetbrains.anko.AnkoLogger 20 | 21 | /** 22 | * 全屏Activity,绝大部分代码是自动生成的, 23 | * 分离出来仅供activity_comic_page使用, 24 | * Created by AoEiuV020 on 2017.09.15-17:38. 25 | */ 26 | @Suppress("MemberVisibilityCanPrivate", "unused") 27 | abstract class ComicPageBaseFullScreenActivity : AppCompatActivity(), AnkoLogger { 28 | private val mHideHandler = Handler() 29 | private val mHidePart2Runnable = Runnable { 30 | // Delayed removal of status and navigation bar 31 | 32 | // Note that some of these constants are new as of API 16 (Jelly Bean) 33 | // and API 19 (KitKat). It is safe to use them, as they are inlined 34 | // at compile-time and do nothing on earlier devices. 35 | viewPager.systemUiVisibility = 36 | View.SYSTEM_UI_FLAG_LOW_PROFILE or 37 | View.SYSTEM_UI_FLAG_FULLSCREEN or 38 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE or 39 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or 40 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or 41 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 42 | } 43 | private val mShowPart2Runnable = Runnable { 44 | // Delayed display of UI elements 45 | app_bar.show() 46 | fullscreen_content_controls.visibility = View.VISIBLE 47 | } 48 | private var mVisible: Boolean = false 49 | private val mHideRunnable = Runnable { hide() } 50 | /** 51 | * Touch listener to use for in-layout UI controls to delay hiding the 52 | * system UI. This is to prevent the jarring behavior of controls going away 53 | * while interacting with activity UI. 54 | */ 55 | private val mDelayHideTouchListener = View.OnTouchListener { _, _ -> 56 | @Suppress("ConstantConditionIf") 57 | if (AUTO_HIDE) { 58 | delayedHide(AUTO_HIDE_DELAY_MILLIS) 59 | } 60 | false 61 | } 62 | 63 | override fun onOptionsItemSelected(item: MenuItem?): Boolean { 64 | item?.itemId.let { 65 | when (it) { 66 | android.R.id.home -> onBackPressed() 67 | } 68 | } 69 | return super.onOptionsItemSelected(item) 70 | } 71 | 72 | override fun onCreate(savedInstanceState: Bundle?) { 73 | super.onCreate(savedInstanceState) 74 | 75 | setContentView(R.layout.activity_comic_page) 76 | setSupportActionBar(toolbar) 77 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 78 | window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) 79 | mVisible = true 80 | } 81 | 82 | override fun onPostCreate(savedInstanceState: Bundle?) { 83 | super.onPostCreate(savedInstanceState) 84 | delayedHide(100) 85 | } 86 | 87 | fun toggle() { 88 | if (mVisible) { 89 | hide() 90 | } else { 91 | show() 92 | } 93 | } 94 | 95 | protected fun hide() { 96 | // Hide UI first 97 | app_bar.hide() 98 | fullscreen_content_controls.visibility = View.GONE 99 | mVisible = false 100 | 101 | // Schedule a runnable to remove the status and navigation bar after a delay 102 | mHideHandler.removeCallbacks(mShowPart2Runnable) 103 | mHideHandler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY.toLong()) 104 | } 105 | 106 | protected fun show() { 107 | // Show the system bar 108 | viewPager.systemUiVisibility = 109 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or 110 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 111 | mVisible = true 112 | 113 | // Schedule a runnable to display UI elements after a delay 114 | mHideHandler.removeCallbacks(mHidePart2Runnable) 115 | mHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY.toLong()) 116 | } 117 | 118 | /** 119 | * Schedules a call to hide() in [delayMillis], canceling any 120 | * previously scheduled calls. 121 | */ 122 | private fun delayedHide(delayMillis: Int) { 123 | mHideHandler.removeCallbacks(mHideRunnable) 124 | mHideHandler.postDelayed(mHideRunnable, delayMillis.toLong()) 125 | } 126 | 127 | companion object { 128 | /** 129 | * Whether or not the system UI should be auto-hidden after 130 | * [AUTO_HIDE_DELAY_MILLIS] milliseconds. 131 | */ 132 | private val AUTO_HIDE = true 133 | 134 | /** 135 | * If [AUTO_HIDE] is set, the number of milliseconds to wait after 136 | * user interaction before hiding the system UI. 137 | */ 138 | private val AUTO_HIDE_DELAY_MILLIS = 3000 139 | 140 | /** 141 | * Some older devices needs a small delay between UI widget updates 142 | * and a change of the status and navigation bar. 143 | */ 144 | private val UI_ANIMATION_DELAY = 300 145 | } 146 | } 147 | 148 | class ComicPageFullScreenRootFrameLayout(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs), AnkoLogger { 149 | @TargetApi(Build.VERSION_CODES.KITKAT_WATCH) 150 | override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets { 151 | findViewById(R.id.navBarBg)?.apply { 152 | layoutParams = layoutParams.apply { height = insets.systemWindowInsetBottom } 153 | } 154 | return super.dispatchApplyWindowInsets(insets) 155 | } 156 | } -------------------------------------------------------------------------------- /app/src/main/java/cc/aoeiuv020/comic/api/manhuatai.kt: -------------------------------------------------------------------------------- 1 | package cc.aoeiuv020.comic.api 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.annotations.SerializedName 5 | import io.reactivex.Observable 6 | import org.jsoup.Jsoup 7 | import java.net.URLEncoder 8 | 9 | 10 | /** 11 | * 漫画台, 12 | * Created by AoEiuV020 on 2017.09.19-11:18:04. 13 | */ 14 | class ManhuataiContext : ComicContext() { 15 | /** 16 | * logo用了精灵图,不好办,截下来发到百度再取连接, 17 | * 地址虽然是jpg结尾,但文件还是支持背景透明的png, 18 | */ 19 | private val site = ComicSite( 20 | name = "漫画台", 21 | baseUrl = "http://www.manhuatai.com", 22 | logo = "https://imgsa.baidu.com/forum/w%3D580/sign=68ba64a46e380cd7e61ea2e59145ad14/45e8c1afa40f4bfb23290540084f78f0f6361810.jpg" 23 | ) 24 | 25 | override fun getComicSite(): ComicSite = site 26 | 27 | override fun getGenres(): List { 28 | val root = getHtml(site.baseUrl) 29 | val elements = root.select("#nav > div > ul > li > span > a:contains(漫)") 30 | return elements.map { 31 | val a = it 32 | ComicGenre(text(a), absHref(a)) 33 | } 34 | } 35 | 36 | override fun getNextPage(genre: ComicGenre): ComicGenre? { 37 | if (isSearchResult(genre)) { 38 | return null 39 | } 40 | val root = getHtml(genre.url) 41 | val a = root.select("a:contains(下一页)").first() 42 | val url = absHref(a) 43 | return if (url.isEmpty()) { 44 | null 45 | } else { 46 | ComicGenre(genre.name, url) 47 | } 48 | } 49 | 50 | override fun getComicList(genre: ComicGenre): List { 51 | if (isSearchResult(genre)) { 52 | val json = Jsoup.connect(genre.url).apply { execute() }.response().body() 53 | val searchResultList = Gson().fromJson(json, SearchResultList::class.java) 54 | return searchResultList.map { 55 | val url = absUrl("/${it.cartoon_id}/") 56 | /** 57 | * 搜索结果没有图片,直接拿大图顶上, 58 | * [getComicDetail] 59 | */ 60 | val img = Observable.fromCallable { 61 | val root = getHtml(url) 62 | src(root.select("#offlinebtn-container > img").first()) 63 | }.map { ComicImage(it) } 64 | ComicListItem(it.cartoon_name, img, url, "${it.cartoon_status_id}, ${it.latest_cartoon_topic_name}") 65 | } 66 | } 67 | val root = getHtml(genre.url) 68 | val elements = root.select("a.sdiv") 69 | return elements.map { 70 | val a = it 71 | val img = it.select("img").first() 72 | val info = it.select("span.a , li:not(.title)").joinToString("\n") { text(it) } 73 | ComicListItem(title(a), img.attr("data-url"), absHref(a), info) 74 | } 75 | } 76 | 77 | class SearchResultList : ArrayList() 78 | 79 | data class SearchResultItem( 80 | @SerializedName("cartoon_id") val cartoon_id: String, // kenan 81 | @SerializedName("cartoon_name") val cartoon_name: String, // 柯南 82 | @SerializedName("cartoon_status_id") val cartoon_status_id: String, // 连载 83 | @SerializedName("latest_cartoon_topic_name") val latest_cartoon_topic_name: String// 996话 84 | ) 85 | 86 | override fun search(name: String): ComicGenre { 87 | return ComicGenre(name, searchUrl(name)) 88 | } 89 | 90 | private fun searchUrl(name: String): String { 91 | val urlEncodedName = URLEncoder.encode(name, "UTF-8") 92 | return absUrl("/getjson.shtml?d=1505801840099&q=$urlEncodedName") 93 | } 94 | 95 | override fun isSearchResult(genre: ComicGenre): Boolean = genre.url.matches(Regex(".*/getjson.*")) 96 | 97 | override fun getComicDetail(comicDetailUrl: ComicDetailUrl): ComicDetail { 98 | val root = getHtml(comicDetailUrl.url) 99 | val name = text(root.select("div.jshtml > ul > li:nth-child(1)")).removePrefix("名称:") 100 | val bigImg = src(root.select("#offlinebtn-container > img").first()) 101 | val info = textWithNewLine(root.select("div.wz.clearfix > div").first(), 1) 102 | val issues = root.select("div.mhlistbody > ul > li > a").map { 103 | val a = it 104 | ComicIssue(text(a), absHref(a)) 105 | }.asReversed() 106 | return ComicDetail(name, bigImg, info, issues) 107 | } 108 | 109 | /** 110 | * 这网站没有根据url跳页码, 111 | * 只能是直接在这里处理出图片地址塞进url里, 112 | 关键js在这里, 113 | http://www.manhuatai.com/static/comicread.js?20170912181829 114 | 开头”;new Function“改成"eval" 115 | 结尾”();“删掉, 116 | 给这网站反混淆, 117 | http://jsbeautifier.org/ 118 | 119 | 关键js是这段,这个反斜杆是为了避免kotlin的警告,\[chapter_id] 120 | n.getPicUrl = function(a) { 121 | var b = mh_info.comic_size || ''; 122 | var c = lines\[chapter_id].use_line; 123 | if (c.indexOf("mhpic") == -1) { 124 | c += ":82" 125 | } 126 | var d = (parseInt(mh_info.startimg) + a - 1) + ".jpg" + b; 127 | var e = "http://" + c + '/comic/' + mh_info.imgpath + d; 128 | return e 129 | }; 130 | */ 131 | override fun getComicPages(comicIssue: ComicIssue): List { 132 | val root = getHtml(comicIssue.url) 133 | val script = root.select("#comiclist > script:nth-child(2)").first() 134 | val mh_info_js = script.toString() 135 | logger.trace { mh_info_js } 136 | val mh_info_json = mh_info_js.pick("