├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── fabric.properties.example ├── proguard-rules.pro └── src │ ├── debug │ └── java │ │ └── com │ │ └── meiji │ │ └── daily │ │ └── SdkManager.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── master.css │ ├── java │ │ └── com │ │ │ └── meiji │ │ │ └── daily │ │ │ ├── AboutActivity.kt │ │ │ ├── App.kt │ │ │ ├── Constant.kt │ │ │ ├── Ext.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MyAppGlideModule.kt │ │ │ ├── bean │ │ │ ├── FooterBean.kt │ │ │ ├── PostsContentBean.kt │ │ │ ├── PostsListBean.kt │ │ │ └── ZhuanlanBean.kt │ │ │ ├── binder │ │ │ ├── FooterViewBinder.kt │ │ │ ├── ItemViewComponent.kt │ │ │ ├── PostsListViewBinder.kt │ │ │ └── ZhuanlanViewBinder.kt │ │ │ ├── data │ │ │ ├── local │ │ │ │ ├── AppDatabase.kt │ │ │ │ ├── converter │ │ │ │ │ └── ZhuanlanBeanConverter.kt │ │ │ │ └── dao │ │ │ │ │ └── ZhuanlanDao.kt │ │ │ └── remote │ │ │ │ └── IApi.kt │ │ │ ├── di │ │ │ ├── component │ │ │ │ ├── AppComponent.kt │ │ │ │ └── CommonActivityComponent.kt │ │ │ ├── module │ │ │ │ └── AppModule.kt │ │ │ └── scope │ │ │ │ ├── ActivityScoped.kt │ │ │ │ └── FragmentScoped.kt │ │ │ ├── listener │ │ │ └── IOnItemClickListener.kt │ │ │ ├── module │ │ │ ├── base │ │ │ │ ├── BaseActivity.kt │ │ │ │ └── BaseFragment.kt │ │ │ ├── postscontent │ │ │ │ ├── PostsContentActivity.kt │ │ │ │ ├── PostsContentComponent.kt │ │ │ │ ├── PostsContentModule.kt │ │ │ │ ├── PostsContentView.kt │ │ │ │ └── PostsContentViewModel.kt │ │ │ ├── postslist │ │ │ │ ├── PostsListActivity.kt │ │ │ │ ├── PostsListComponent.kt │ │ │ │ ├── PostsListModule.kt │ │ │ │ ├── PostsListView.kt │ │ │ │ └── PostsListViewModel.kt │ │ │ ├── shareadd │ │ │ │ ├── ShareAddActivity.kt │ │ │ │ └── ShareAddComponent.kt │ │ │ ├── useradd │ │ │ │ ├── UserAddComponent.kt │ │ │ │ ├── UserAddModule.kt │ │ │ │ ├── UserAddView.kt │ │ │ │ └── UserAddViewModel.kt │ │ │ └── zhuanlan │ │ │ │ ├── ZhuanlanComponent.kt │ │ │ │ ├── ZhuanlanModule.kt │ │ │ │ ├── ZhuanlanView.kt │ │ │ │ └── ZhuanlanViewModel.kt │ │ │ └── util │ │ │ ├── DiffCallback.kt │ │ │ ├── ErrorAction.kt │ │ │ ├── HttpLoggingInterceptor.kt │ │ │ ├── JsonUtil.kt │ │ │ ├── NetWorkUtil.kt │ │ │ ├── OnLoadMoreListener.kt │ │ │ ├── RecyclerViewUtil.kt │ │ │ ├── RetrofitFactory.kt │ │ │ ├── RetrofitHelper.kt │ │ │ ├── RxBus.java │ │ │ ├── RxBusHelper.kt │ │ │ ├── SettingHelper.kt │ │ │ └── SettingUtil.java │ └── res │ │ ├── drawable-v21 │ │ ├── ic_copyright.xml │ │ ├── ic_description.xml │ │ ├── ic_github.xml │ │ ├── ic_history.xml │ │ ├── ic_info.xml │ │ ├── ic_menu_camera.xml │ │ ├── ic_menu_gallery.xml │ │ ├── ic_menu_manage.xml │ │ ├── ic_menu_send.xml │ │ ├── ic_menu_share.xml │ │ ├── ic_menu_slideshow.xml │ │ ├── ic_person.xml │ │ ├── ic_photo_add.xml │ │ ├── ic_photo_color_chooser.xml │ │ ├── ic_photo_computer.xml │ │ ├── ic_photo_emotion.xml │ │ ├── ic_photo_financial.xml │ │ ├── ic_photo_info.xml │ │ ├── ic_photo_music.xml │ │ ├── ic_photo_share.xml │ │ ├── ic_photo_share_white.xml │ │ └── nav_header.png │ │ ├── drawable │ │ ├── error_image.png │ │ ├── ic_menu_cloud.xml │ │ ├── ic_photo_camera.xml │ │ ├── ic_share.xml │ │ └── side_nav_bar.xml │ │ ├── layout │ │ ├── activity_about.xml │ │ ├── activity_main.xml │ │ ├── activity_postscontent.xml │ │ ├── activity_postslist.xml │ │ ├── container.xml │ │ ├── content_main.xml │ │ ├── fragment_useradd.xml │ │ ├── fragment_zhuanlan.xml │ │ ├── item_loading.xml │ │ ├── item_postlist.xml │ │ ├── item_switch.xml │ │ ├── item_zhuanlan.xml │ │ ├── nav_header_main.xml │ │ └── toolbar.xml │ │ ├── menu │ │ ├── activity_main_drawer.xml │ │ └── menu_about.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-night │ │ └── colors.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── drawables.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── zhuanlan_ids.xml │ └── release │ └── java │ └── com.meiji.daily │ └── SdkManager.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── daily_1.gif ├── daily_2.gif ├── daily_3.gif └── daily_4.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .DS_Store 5 | /build 6 | /captures 7 | .externalNativeBuild 8 | libs/ 9 | /.idea/* 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: oraclejdk8 3 | sudo: false 4 | android: 5 | components: 6 | - tools 7 | - build-tools-27.0.3 8 | - android-27 9 | - extra-android-m2repository 10 | - extra-android-support 11 | licenses: 12 | - android-sdk-license-.+ 13 | - android-sdk-preview-license-.+ 14 | before_cache: 15 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 16 | cache: 17 | directories: 18 | - "$HOME/.gradle/caches/" 19 | - "$HOME/.gradle/wrapper/" 20 | before_install: 21 | - yes | sdkmanager "platforms;android-27" 22 | - rm -f gradle.properties 23 | - chmod +x gradlew 24 | script: 25 | - "./gradlew assembleRelease" 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Platform][1]][2] [![Build Status][3]][4] [![Release][5]][6] [![GitHub license][7]][8] [![Downloads][9]][10] 2 | 3 | [1]:https://img.shields.io/badge/platform-Android-blue.svg 4 | [2]:https://github.com/iMeiji/Daily 5 | [3]:https://travis-ci.org/iMeiji/Daily.svg?branch=master 6 | [4]:https://travis-ci.org/iMeiji/Daily 7 | [5]:https://img.shields.io/github/release/iMeiji/Daily.svg 8 | [6]:https://github.com/iMeiji/Daily/releases/latest 9 | [7]:https://img.shields.io/badge/license-Apache%202-blue.svg 10 | [8]:https://github.com/iMeiji/Daily/blob/master/LICENSE 11 | [9]:https://img.shields.io/github/downloads/iMeiji/Daily/total.svg?maxAge=1800 12 | [10]:https://github.com/iMeiji/Daily/releases 13 | 14 | 15 | ## Daily 16 | 第三方[知乎专栏](https://zhuanlan.zhihu.com/)Android App,风格采用了Material Design,有 Kotlin 和 Java 两个版本,切换分支即可查看 17 | 18 | 19 | 20 | 21 | ### 代码特性 22 | - 使用 Google I/O'17 发布的 [Android Architecture Components](https://developer.android.com/topic/libraries/architecture/index.html) 架构组件 23 | - 使用 Lifecycle,LiveData,ViewModel 以及 Room,非常优雅的让数据与界面交互,并做一些持久化的东西,高度解耦,自动管理生命周期,而且不用担心内存泄漏的问题 24 | - 代码入手难度极低,源于对于"简单直观、干净清晰"理念的把握和追求 25 | - 基于 [MultiType](https://github.com/drakeet/MultiType),全局没有新创建任何一个 Adapter 类 26 | - 模块清晰,聚合有度 27 | - 使用最新 RxJava 2,Glide 4 新特征 28 | - 使用 DiffUtil 优雅实现 notifyDataSetChanged 29 | - 运用 Kotlin 语法糖,精简代码 30 | 31 | 32 | 33 | 34 | ### 已实现的功能 35 | - 专栏内容阅读 36 | 37 | - 添加自定义专栏 38 | 39 | - 删除自定义专栏 40 | 41 | - 自定义主题颜色 42 | 43 | - 仿知乎动态切换夜间模式(无需recreate) 44 | 45 | - 集成 Farbic 自动上传奔溃 log,方便开发者发现 bug 46 | 47 | ​ 48 | 49 | 50 | 51 | ### 待实现的功能 52 | - 收藏夹 53 | 54 | - 清除缓存 55 | 56 | ​ 57 | 58 | 59 | 60 | ### 更新日志 61 | ``` 62 | 2018-2-7 63 | 完成 Kotlin 版本 64 | 65 | 2017-12-17 66 | 更新 RxJava 2,Glide 4 67 | 集成 Fabric SDK 68 | 69 | 2017-12-7 70 | 使用 Android Architecture Components 架构,移除 MVP 架构,告别繁琐的接口调用 71 | 使用 Room 操作 SQL 72 | 73 | 2017-7-13 74 | 添加夜间模式(仿知乎) 75 | 76 | 2017-7-11 77 | 引入 Dagger2 78 | 79 | 2017-6-8 80 | 封装加载更多, Diff等 81 | 82 | 2017-6-7 83 | 引入 MultiTypeAdapter, DiffUtil 84 | 85 | 2017-5-5 86 | 引入 RxLiftcycle 87 | 88 | 2017-5-2 89 | 增加缓存机制 90 | Gradle 差异化构建 91 | 92 | 2017-4-25 93 | 引入 Retrofit, RxJava 94 | 95 | 2017-4-22 96 | 重启项目 97 | ``` 98 | 99 | 100 | 101 | ### API 102 | 103 | [数据来源](https://github.com/shanelau/zhihu) 104 | 105 | 106 | 107 | ### 截图 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | ### 下载 117 | 118 | [GitHub Release](https://github.com/iMeiji/Daily/releases) 119 | 120 | 121 | 122 | ### 编译 123 | 124 | 1. 导入项目 125 | 2. 复制 `fabric.properties.example` 并重命名为 `fabric.properties` 126 | 3. 运行项目 127 | 128 | 129 | 130 | 131 | ### 开源库 132 | 133 | - [Android Support Libraries](https://developer.android.com/topic/libraries/support-library/index.html) 134 | - [Android Architecture Components](https://developer.android.com/topic/libraries/architecture/index.html) 135 | - [Material Dialogs](https://github.com/afollestad/material-dialogs) 136 | - [OkHttp](https://github.com/square/okhttp) 137 | - [Gson](https://github.com/google/gson) 138 | - [Glide](https://github.com/bumptech/glide) 139 | - [CircleImageView](https://github.com/hdodenhof/CircleImageView) 140 | - [LicensesDialog](https://github.com/PSDev/LicensesDialog) 141 | - [Retrofit](https://github.com/square/retrofit) 142 | - [PersistentCookieJar](https://github.com/franmontiel/PersistentCookieJar) 143 | - [RxJava](https://github.com/ReactiveX/RxJava) 144 | - [RxAndroid](https://github.com/ReactiveX/RxAndroid) 145 | - [MultiType](https://github.com/drakeet/MultiType) 146 | - [Dagger 2](https://github.com/google/dagger) 147 | 148 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | fabric.properties 3 | /release -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'io.fabric' 3 | apply plugin: 'kotlin-android' 4 | apply plugin: 'kotlin-android-extensions' 5 | apply plugin: 'kotlin-kapt' 6 | apply plugin: 'com.github.ben-manes.versions' 7 | 8 | android { 9 | compileSdkVersion 27 10 | buildToolsVersion '27.0.3' 11 | defaultConfig { 12 | applicationId "com.meiji.daily" 13 | minSdkVersion 16 14 | targetSdkVersion 27 15 | versionCode 2 16 | versionName '1.45' 17 | multiDexEnabled true 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled true //设为false即可断点调试 22 | shrinkResources true //自动移除不用资源 23 | zipAlignEnabled true 24 | debuggable false 25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | ext.enableCrashlytics = true 27 | applicationVariants.all { variant -> 28 | variant.outputs.all { output -> 29 | def outputFile = output.outputFile 30 | if (outputFile != null && outputFile.name.endsWith('.apk')) { 31 | // 输出apk名称为daily-release-v1.0.apk 32 | def fileName = "daily-release-v${defaultConfig.versionName}.apk" 33 | outputFileName = fileName 34 | } 35 | } 36 | } 37 | } 38 | debug { 39 | minifyEnabled false 40 | debuggable true 41 | // Disable fabric build ID generation for debug builds 42 | ext.enableCrashlytics = false 43 | } 44 | } 45 | flavorDimensions "default" 46 | productFlavors { 47 | dev { 48 | minSdkVersion 21 49 | dimension "default" 50 | } 51 | } 52 | compileOptions { 53 | sourceCompatibility JavaVersion.VERSION_1_8 54 | targetCompatibility JavaVersion.VERSION_1_8 55 | } 56 | } 57 | 58 | kapt { 59 | useBuildCache = true 60 | } 61 | 62 | dependencies { 63 | implementation fileTree(include: ['*.jar'], dir: 'libs') 64 | 65 | // support library 66 | implementation "com.android.support:appcompat-v7:${support_version}" 67 | implementation "com.android.support:support-v13:${support_version}" 68 | implementation "com.android.support:design:${support_version}" 69 | implementation "com.android.support:recyclerview-v7:${support_version}" 70 | implementation "com.android.support:cardview-v7:${support_version}" 71 | implementation "com.android.support:support-annotations:${support_version}" 72 | 73 | // implementation "com.android.support:support-v4:${support_version}" 74 | implementation 'com.android.support:multidex:1.0.3' 75 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 76 | 77 | // MD dialog 78 | implementation 'com.afollestad.material-dialogs:commons:0.9.6.0' 79 | 80 | // Glide 81 | implementation 'com.github.bumptech.glide:glide:4.6.1' 82 | kapt 'com.github.bumptech.glide:compiler:4.6.1' 83 | implementation('com.github.bumptech.glide:okhttp-integration:4.4.0') { 84 | exclude group: 'com.squareup.okhttp3', module: 'okhttp' 85 | } 86 | 87 | // 圆形图像 88 | implementation 'de.hdodenhof:circleimageview:2.2.0' 89 | 90 | // license dialog 91 | implementation 'de.psdev.licensesdialog:licensesdialog:1.8.3' 92 | 93 | // retrofit 2 94 | implementation 'com.squareup.retrofit2:retrofit:2.4.0' 95 | implementation 'com.squareup.retrofit2:converter-moshi:2.4.0' 96 | implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0' 97 | 98 | // Moshi 99 | implementation 'com.squareup.moshi:moshi-kotlin:1.5.0' 100 | 101 | // 持久化 Cookie 102 | implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1' 103 | 104 | // RxJava 2 105 | implementation 'io.reactivex.rxjava2:rxjava:2.1.12' 106 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' 107 | 108 | // okhttp 3 109 | implementation 'com.squareup.okhttp3:okhttp:3.10.0' 110 | 111 | // debugImplementation 'com.squareup.okhttp3:logging-interceptor:3.9.1' 112 | 113 | // stetho 114 | debugImplementation 'com.facebook.stetho:stetho:1.5.0' 115 | 116 | // MultiType 117 | implementation 'me.drakeet.multitype:multitype:3.4.4' 118 | 119 | // Lifecycles 120 | implementation "android.arch.lifecycle:runtime:$arch_version" 121 | kapt "android.arch.lifecycle:compiler:$arch_version" 122 | implementation "android.arch.lifecycle:common-java8:$arch_version" 123 | 124 | // LiveData and ViewModel 125 | implementation "android.arch.lifecycle:extensions:$arch_version" 126 | 127 | // Room 128 | implementation 'android.arch.persistence.room:runtime:1.0.0' 129 | kapt "android.arch.persistence.room:compiler:1.0.0" 130 | implementation 'android.arch.persistence.room:rxjava2:1.0.0' 131 | 132 | // fabric 133 | implementation('com.crashlytics.sdk.android:crashlytics:2.9.1@aar') { 134 | transitive = true 135 | } 136 | 137 | // Dagger 2 138 | implementation 'com.google.dagger:dagger:2.15' 139 | kapt 'com.google.dagger:dagger-compiler:2.15' 140 | 141 | // implementation 'com.google.dagger:dagger-android:2.13' 142 | 143 | // implementation 'com.google.dagger:dagger-android-support:2.13' 144 | 145 | // annotationProcessor 'com.google.dagger:dagger-android-processor:2.13' 146 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 147 | } 148 | 149 | 150 | afterEvaluate { 151 | initFabricPropertiesIfNeeded() 152 | } 153 | 154 | def initFabricPropertiesIfNeeded() { 155 | def propertiesFile = file('fabric.properties') 156 | if (!propertiesFile.exists()) { 157 | def commentMessage = "This is autogenerated fabric property from system environment to prevent key to be committed to source control." 158 | ant.propertyfile(file: "fabric.properties", comment: commentMessage) { 159 | entry(key: "apiSecret", value: crashlyticsApiSecret) 160 | entry(key: "apiKey", value: crashlyticsApiKey) 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /app/fabric.properties.example: -------------------------------------------------------------------------------- 1 | #Contains API Secret used to validate your application. Commit to internal source control; avoid making secret public. 2 | #Sat Dec 16 18:29:11 CST 2017 3 | apiSecret=123 4 | apiKey=123 -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Users\Meiji\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | -keep class !android.support.v7.internal.view.menu.**,** {*;} 20 | -ignorewarnings 21 | 22 | # support-v7-appcompat 23 | -keep public class android.support.v7.widget.** { *; } 24 | -keep public class android.support.v7.internal.widget.** { *; } 25 | -keep public class android.support.v7.internal.view.menu.** { *; } 26 | 27 | -keep public class * extends android.support.v4.view.ActionProvider { 28 | public (android.content.Context); 29 | } 30 | 31 | 32 | # support-v7-cardview 33 | -keep class android.support.v7.widget.RoundRectDrawable { *; } 34 | 35 | 36 | # support-design 37 | -dontwarn android.support.design.** 38 | -keep class android.support.design.** { *; } 39 | -keep interface android.support.design.** { *; } 40 | -keep public class android.support.design.R$* { *; } 41 | 42 | 43 | # Retrofit 44 | # Platform calls Class.forName on types which do not exist on Android to determine platform. 45 | -dontnote retrofit2.Platform 46 | # Platform used when running on Java 8 VMs. Will not be used at runtime. 47 | -dontwarn retrofit2.Platform$Java8 48 | # Retain generic type information for use by reflection by converters and adapters. 49 | -keepattributes Signature 50 | # Retain declared checked exceptions for use by a Proxy instance. 51 | -keepattributes Exceptions 52 | 53 | 54 | # PersistentCookieJar 55 | -dontwarn com.franmontiel.persistentcookiejar.** 56 | -keep class com.franmontiel.persistentcookiejar.** 57 | -keepclassmembers class * implements java.io.Serializable { 58 | static final long serialVersionUID; 59 | private static final java.io.ObjectStreamField[] serialPersistentFields; 60 | !static !transient ; 61 | private void writeObject(java.io.ObjectOutputStream); 62 | private void readObject(java.io.ObjectInputStream); 63 | java.lang.Object writeReplace(); 64 | java.lang.Object readResolve(); 65 | } 66 | 67 | 68 | # Glide 69 | -keep public class * implements com.bumptech.glide.module.GlideModule 70 | -keep public class * extends com.bumptech.glide.module.AppGlideModule 71 | -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { 72 | **[] $VALUES; 73 | public *; 74 | } 75 | # for DexGuard only 76 | #-keepresourcexmlelements manifest/application/meta-data@value=GlideModule 77 | 78 | 79 | ## OkHttp 80 | -dontwarn okio.** 81 | 82 | ## Crashlytics 83 | -keepattributes *Annotation* 84 | -keepattributes SourceFile,LineNumberTable 85 | -------------------------------------------------------------------------------- /app/src/debug/java/com/meiji/daily/SdkManager.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily 2 | 3 | import android.content.Context 4 | 5 | import com.facebook.stetho.Stetho 6 | import com.meiji.daily.util.HttpLoggingInterceptor 7 | 8 | import okhttp3.OkHttpClient 9 | 10 | /** 11 | * Created by Meiji on 2017/5/2. 12 | */ 13 | 14 | object SdkManager { 15 | fun initStetho(context: Context) { 16 | Stetho.initializeWithDefaults(context) 17 | } 18 | 19 | fun initInterceptor(builder: OkHttpClient.Builder): OkHttpClient.Builder { 20 | val interceptor = HttpLoggingInterceptor() 21 | interceptor.mLevel = HttpLoggingInterceptor.Level.BODY 22 | builder.addInterceptor(interceptor) 23 | return builder 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 44 | 45 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/AboutActivity.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.support.v7.widget.Toolbar 8 | import android.view.Menu 9 | import android.view.MenuItem 10 | import android.view.View 11 | import com.afollestad.materialdialogs.DialogAction 12 | import com.afollestad.materialdialogs.MaterialDialog 13 | import com.afollestad.materialdialogs.Theme 14 | import com.meiji.daily.module.base.BaseActivity 15 | import de.psdev.licensesdialog.LicensesDialog 16 | import de.psdev.licensesdialog.licenses.ApacheSoftwareLicense20 17 | import de.psdev.licensesdialog.licenses.MITLicense 18 | import de.psdev.licensesdialog.model.Notice 19 | import de.psdev.licensesdialog.model.Notices 20 | import kotlinx.android.synthetic.main.activity_about.* 21 | 22 | /** 23 | * Created by Meiji on 2016/12/3. 24 | */ 25 | 26 | class AboutActivity : BaseActivity(), View.OnClickListener { 27 | 28 | override fun attachLayoutId() = R.layout.activity_about 29 | 30 | override fun initData(savedInstanceState: Bundle?) { 31 | packageManager?.getPackageInfo(packageName, 0)?.versionName.let { 32 | tv_versionCode.text = it 33 | } 34 | } 35 | 36 | override fun initViews() { 37 | initToolBar(toolbar as Toolbar, true, null) 38 | 39 | cl_changelog.setOnClickListener(this) 40 | cl_developers.setOnClickListener(this) 41 | cl_licenses.setOnClickListener(this) 42 | cl_source.setOnClickListener(this) 43 | cl_copyright.setOnClickListener(this) 44 | } 45 | 46 | override fun onClick(view: View) { 47 | when (view.id) { 48 | R.id.cl_changelog -> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.changelog_url)))) 49 | R.id.cl_developers -> { 50 | val devDialog = MaterialDialog.Builder(this) 51 | .title(R.string.about_developers_label) 52 | .content(R.string.about_developers) 53 | .theme(if (mSettingHelper.isNightMode) Theme.DARK else Theme.LIGHT) 54 | .positiveText(android.R.string.ok) 55 | .build() 56 | devDialog.getActionButton(DialogAction.POSITIVE).setTextColor(mSettingHelper.color) 57 | devDialog.show() 58 | } 59 | R.id.cl_licenses -> createLicenseDialog() 60 | R.id.cl_source -> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.source_code_url)))) 61 | R.id.cl_copyright -> { 62 | val copyRightDialog = MaterialDialog.Builder(this) 63 | .title(R.string.action_copyright) 64 | .theme(if (mSettingHelper.isNightMode) Theme.DARK else Theme.LIGHT) 65 | .content(R.string.copyright_content) 66 | .positiveText(R.string.md_got_it) 67 | .build() 68 | copyRightDialog.getActionButton(DialogAction.POSITIVE).setTextColor(mSettingHelper.color) 69 | copyRightDialog.show() 70 | } 71 | } 72 | } 73 | 74 | private fun createLicenseDialog() { 75 | val notices = Notices() 76 | notices.addNotice(Notice("Material Dialogs", "https://github.com/afollestad/material-dialogs", "Copyright (c) 2014-2016 Aidan Michael Follestad", MITLicense())) 77 | notices.addNotice(Notice("OkHttp", "https://github.com/square/okhttp", "Copyright 2016 Square, Inc.", ApacheSoftwareLicense20())) 78 | notices.addNotice(Notice("Gson", "https://github.com/google/sGson", "Copyright 2008 Google Inc.", ApacheSoftwareLicense20())) 79 | notices.addNotice(Notice("Glide", "https://github.com/bumptech/glide", "Sam Judd - @sjudd on GitHub, @samajudd on Twitter", ApacheSoftwareLicense20())) 80 | notices.addNotice(Notice("CircleImageView", "https://github.com/hdodenhof/CircleImageView", "Copyright 2014 - 2016 Henning Dodenhof", ApacheSoftwareLicense20())) 81 | 82 | LicensesDialog.Builder(this) 83 | .setNotices(notices) 84 | .setIncludeOwnLicense(true) 85 | .build() 86 | .show() 87 | } 88 | 89 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 90 | menuInflater.inflate(R.menu.menu_about, menu) 91 | return super.onCreateOptionsMenu(menu) 92 | } 93 | 94 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 95 | when (item.itemId) { 96 | R.id.share -> { 97 | val shareIntent = Intent() 98 | .setAction(Intent.ACTION_SEND) 99 | .setType("text/plain") 100 | .putExtra(Intent.EXTRA_TEXT, getString(R.string.share_app_text) + getString(R.string.source_code_url)) 101 | startActivity(Intent.createChooser(shareIntent, getString(R.string.share_to))) 102 | } 103 | } 104 | return super.onOptionsItemSelected(item) 105 | } 106 | 107 | companion object { 108 | fun start(context: Context) { 109 | val starter = Intent(context, AboutActivity::class.java) 110 | context.startActivity(starter) 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/App.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.support.multidex.MultiDex 6 | import com.crashlytics.android.Crashlytics 7 | import com.crashlytics.android.core.CrashlyticsCore 8 | import com.meiji.daily.di.component.AppComponent 9 | import com.meiji.daily.di.component.DaggerAppComponent 10 | import io.fabric.sdk.android.Fabric 11 | 12 | /** 13 | * Created by Meiji on 2016/12/7. 14 | */ 15 | 16 | class App : Application() { 17 | 18 | override fun attachBaseContext(base: Context) { 19 | super.attachBaseContext(base) 20 | MultiDex.install(this) 21 | } 22 | 23 | override fun onCreate() { 24 | super.onCreate() 25 | sAppComponent = DaggerAppComponent 26 | .builder() 27 | .application(this) 28 | .context(applicationContext) 29 | .build() 30 | sAppComponent.inject(this) 31 | 32 | sAppContext = applicationContext 33 | if (BuildConfig.DEBUG) { 34 | SdkManager.initStetho(this) 35 | } 36 | 37 | initFabric() 38 | } 39 | 40 | private fun initFabric() { 41 | // Set up Crashlytics, disabled for debug builds 42 | val crashlyticsKit = Crashlytics.Builder() 43 | .core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()) 44 | .build() 45 | 46 | // Initialize Fabric with the debug-disabled crashlytics. 47 | Fabric.with(this, crashlyticsKit) 48 | } 49 | 50 | companion object { 51 | 52 | lateinit var sAppContext: Context 53 | lateinit var sAppComponent: AppComponent 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/Constant.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily 2 | 3 | /** 4 | * Created by Meiji on 2018/1/25. 5 | */ 6 | 7 | object Constant { 8 | 9 | const val TYPE_PRODUCT = 0 10 | const val TYPE_MUSIC = 1 11 | const val TYPE_LIFE = 2 12 | const val TYPE_EMOTION = 3 13 | const val TYPE_FINANCE = 4 14 | const val TYPE_ZHIHU = 5 15 | const val TYPE_USERADD = 6 16 | 17 | object RxBusEvent { 18 | const val REFRESHUI = "refreshUI" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/Ext.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily 2 | 3 | import io.reactivex.android.schedulers.AndroidSchedulers 4 | import io.reactivex.schedulers.Schedulers 5 | 6 | 7 | val io get() = Schedulers.io() 8 | val mainThread get() = AndroidSchedulers.mainThread()!! -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/MyAppGlideModule.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily 2 | 3 | import com.bumptech.glide.annotation.GlideModule 4 | import com.bumptech.glide.module.AppGlideModule 5 | 6 | /** 7 | * Created by Meiji on 2017/12/14. 8 | */ 9 | @GlideModule 10 | class MyAppGlideModule : AppGlideModule() -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/bean/FooterBean.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.bean 2 | 3 | /** 4 | * Created by Meiji on 2017/6/7. 5 | */ 6 | 7 | class FooterBean 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/bean/PostsContentBean.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.bean 2 | 3 | /** 4 | * Created by Meiji on 2018/2/9. 5 | */ 6 | 7 | data class PostsContentBean( 8 | val isTitleImageFullScreen: Boolean, //true 9 | val rating: String, //none 10 | val titleImage: String, //https://pic2.zhimg.com/v2-aea21f51ef694429a9270a45b49c66d3_r.jpg 11 | val links: Links, 12 | val reviewers: List, 13 | val topics: List, 14 | val admin_closed_comment: Boolean, //false 15 | val titleImageSize: TitleImageSize, 16 | val href: String, ///api/posts/33717055 17 | val collapsedCount: Int, //0 18 | val excerptTitle: String, 19 | val author: Author, 20 | val column: Column, 21 | val tipjarState: String, //inactivated 22 | val content: String, //

【知群】是做“连接”的知识社群产品,连接学习者和“分享/招聘者”。在过去的半年时间里,【知群】以学习社群的方式与几十万互联网从业人员建立了连接,这其中包括了近千名资深专家、总监、副总裁以及一线实战的创始人所在的互联网学习圈,以及以UI和UX为主的设计师群体等。

具体来说,我们在做的事情是用社群的方式帮助互联网人学习、成长、找工作……

1.梳理知识体系,研发系统的、高质量的课程,避免过于碎片化的知识,让大家只要愿意花时间,就能学到东西;

2.邀请互联网行业的资深专业人士,来帮助正在成长中的人们。这些行业里的专业人士、大牛大咖们,刚好都和我们很熟,也愿意来指导行业中的后来者;

3.注重圈子的力量,培养社群,提供招聘、内推等的信息和机会,不断消除资源合作、招聘内推之中的信息不对称。


年后的3月份,【知群】将会上线 2018 年第一场「限时闪聘」活动,这是我们在招聘服务上进行的一次探索,让企业方和求职者实现更高效的对接。招聘会的规则是 ——


候选人在规定的时间内投递简历

团队负责人会在3小时内回复你的求职申请


以下是第一场「限时闪聘」活动的部分职位信息,我们预先提供给大家。因为是第一场,有些互联网圈内的朋友还不知道我们在做这件事,所以我们选择提前曝光这些职位,这样对职位感兴趣的朋友就不会错过这些工作机会。

每次参与闪聘活动的企业方均为一线互联网公司,如阿里巴巴、腾讯、美团点评等。如果你对这些职位感兴趣,可以先和我们进行交流,我们会通过社群里的人际关系链帮助大家与团队负责人更好地建立连接。

想找工作的朋友,可以先将简历发送到


wang.xin@izhiqun.com


【知群】收到简历后,会先和大家接触,了解大家的求职意向、沟通职位具体要求等,让我们一起把知群的内推服务做得更好。或者可以添加助手Belle的微信,微信号:Belle-Pearly,具体沟通。

23 | val state: String, //published 24 | val annotation_action: List, 25 | val sourceUrl: String, 26 | val pageCommentsCount: Int, //1 27 | val canComment: Boolean, //false 28 | val has_publishing_draft: Boolean, //false 29 | val snapshotUrl: String, 30 | val slug: Int, //33717055 31 | val publishedTime: String, //2018-02-09T12:13:22+08:00 32 | val url: String, ///p/33717055 33 | val title: String, //年后想跳槽?我们可以帮你直接内推至阿里巴巴、腾讯、华为…… 34 | val lastestLikers: List, 35 | val summary: String, //【知群】是做“连接”的知识社群产品,连接学习者和“分享/招聘者”。在过去的半年时间里,【知群】以学习社群的方式与几十万互联网从业人员建立了连接,这其中包括了近千名资深专家、总监、副总裁以及一线实战的创始人所在的互联网学习圈,以及以UI和UX为… 36 | val reviewingCommentsCount: Int, //0 37 | val meta: Meta, 38 | val annotation_detail: Any, //null 39 | val commentPermission: String, //anyone 40 | val commentsCount: Int, //1 41 | val likesCount: Int //25 42 | ) { 43 | data class LastestLiker( 44 | val bio: String, //设计师 45 | val isFollowing: Boolean, //false 46 | val hash: String, //bc27a79a243ad1bc23d3619275acfbd0 47 | val uid: Long, //663377478475845632 48 | val isOrg: Boolean, //false 49 | val slug: String, //gang-tie-di 50 | val isFollowed: Boolean, //false 51 | val description: String, 52 | val name: String, //钢铁迪 53 | val profileUrl: String, //https://www.zhihu.com/people/gang-tie-di 54 | val avatar: Avatar, 55 | val isOrgWhiteList: Boolean, //false 56 | val isBanned: Boolean //false 57 | ) { 58 | data class Avatar( 59 | val id: String, //9dcc7c682f39e3cf97a504bccf74c632 60 | val template: String //https://pic3.zhimg.com/{id}_{size}.jpg 61 | ) 62 | } 63 | 64 | data class TitleImageSize( 65 | val width: Int, //1920 66 | val height: Int //1285 67 | ) 68 | 69 | data class Links( 70 | val comments: String ///api/posts/33717055/comments 71 | ) 72 | 73 | data class Meta( 74 | val previous: Previous, 75 | val next: Any //null 76 | ) { 77 | data class Previous( 78 | val isTitleImageFullScreen: Boolean, //false 79 | val rating: String, //none 80 | val titleImage: String, 81 | val links: Links, 82 | val topics: List, 83 | val admin_closed_comment: Boolean, //false 84 | val href: String, ///api/posts/33716448 85 | val excerptTitle: String, 86 | val author: Author, 87 | val content: String, //

我们最近在和一些面向设计师、产品经理的工具软件团队合作,觉得有一些很有意思的模式,希望能够和更多的团队一起合作。

如果有这个方面相关的开发者,产品在国内拥有相当用户量的,欢迎联系我。

我们覆盖了非常大的设计师、产品经理用户群体,在帮助更多的人在专业方面成长,同时也希望能够帮到更多致力于做这个领域工具产品、为专业人群服务的团队。

直接在知乎私信我就可以。

88 | val state: String, //published 89 | val sourceUrl: String, 90 | val pageCommentsCount: Int, //0 91 | val canComment: Boolean, //false 92 | val snapshotUrl: String, 93 | val slug: Int, //33716448 94 | val publishedTime: String, //2018-02-09T11:52:11+08:00 95 | val url: String, ///p/33716448 96 | val title: String, //寻找面向设计师、产品经理的工具软件、App 合作 97 | val summary: String, //我们最近在和一些面向设计师、产品经理的工具软件团队合作,觉得有一些很有意思的模式,希望能够和更多的团队一起合作。如果有这个方面相关的开发者,产品在国内拥有相当用户量的,欢迎联系我。我们覆盖了非常大的设计师、产品经理用户群体,在帮助更多的人… 98 | val reviewingCommentsCount: Int, //0 99 | val meta: Meta, 100 | val commentPermission: String, //anyone 101 | val commentsCount: Int, //3 102 | val likesCount: Int //21 103 | ) { 104 | data class Links( 105 | val comments: String ///api/posts/33716448/comments 106 | ) 107 | 108 | data class Topic( 109 | val url: String, //https://www.zhihu.com/topic/19551325 110 | val id: String, //19551325 111 | val name: String //产品经理 112 | ) 113 | 114 | data class Author( 115 | val bio: String, //马力在招聘:zhuanlan.zhihu.com/p/31904197 116 | val isFollowing: Boolean, //false 117 | val hash: String, //c6e85ba5d5999df4c5ce2f2903b1ce0e 118 | val uid: Long, //26680571723776 119 | val isOrg: Boolean, //false 120 | val slug: String, //mali 121 | val isFollowed: Boolean, //false 122 | val description: String, //最美应用创始人 产品经理 设计师,欢迎关注微博:@Ma_Li | 他在好奇的注视着这个饶有趣味的世界 | 感谢在评选知乎年度荣誉会员时为我投票的各位,一起认真!| 马力的互联网学习圈:http://mali.brixd.com | 文章索引: https://zhuanlan.zhihu.com/p/25493627 123 | val name: String, //马力 124 | val profileUrl: String, //https://www.zhihu.com/people/mali 125 | val avatar: Avatar, 126 | val isOrgWhiteList: Boolean, //false 127 | val isBanned: Boolean //false 128 | ) { 129 | data class Avatar( 130 | val id: String, //ba332a401 131 | val template: String //https://pic2.zhimg.com/{id}_{size}.jpg 132 | ) 133 | } 134 | 135 | data class Meta( 136 | val previous: Any, //null 137 | val next: Any //null 138 | ) 139 | } 140 | } 141 | 142 | data class Author( 143 | val bio: String, //无法定义 144 | val isFollowing: Boolean, //false 145 | val hash: String, //0ddb38d93b1c1d6c91cfb94a1d46ef99 146 | val uid: Long, //33147160887296 147 | val isOrg: Boolean, //false 148 | val slug: String, //christinehu 149 | val isFollowed: Boolean, //false 150 | val description: String, //好好活下去……每天都有新打击 151 | val name: String, //夏漱香菜 152 | val badge: Badge, 153 | val profileUrl: String, //https://www.zhihu.com/people/christinehu 154 | val avatar: Avatar, 155 | val isOrgWhiteList: Boolean, //false 156 | val isBanned: Boolean //false 157 | ) { 158 | data class Avatar( 159 | val id: String, //v2-0efada2f1686480f2163224252b8f824 160 | val template: String //https://pic3.zhimg.com/{id}_{size}.jpg 161 | ) 162 | 163 | data class Badge( 164 | val identity: Any, //null 165 | val best_answerer: Any //null 166 | ) 167 | } 168 | 169 | data class Topic( 170 | val url: String, //https://www.zhihu.com/topic/19551771 171 | val id: String, //19551771 172 | val name: String //求职 173 | ) 174 | 175 | data class Column( 176 | val slug: String, //design 177 | val name: String //可能性 | 产品与大设计 178 | ) 179 | } -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/bean/PostsListBean.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.bean 2 | 3 | /** 4 | * Created by Meiji on 2018/2/9. 5 | */ 6 | 7 | data class PostsListBean( 8 | val isTitleImageFullScreen: Boolean, //false 9 | val rating: String, //none 10 | val sourceUrl: String, 11 | val publishedTime: String, //2018-01-29T15:45:38+08:00 12 | val links: Links, 13 | val author: Author, 14 | val url: String, ///p/33398689 15 | val title: String, //发布了一款叫做「抽奖助手」的小程序 16 | val titleImage: String, //https://pic4.zhimg.com/v2-de4b8114e8408d5265503c8b41f59f85_r.jpg 17 | val summary: String, 18 | val content: String, //

无码科技团队前不久发布了一款叫做「抽奖助手」的小程序。目前在用户那边反馈还不错。

这个小程序主要解决如下三个场景的需求:

  • 公众号作者抽奖活动
  • 微信群里抽奖
  • 年会抽奖

先说第一个需求,也是我自己的需求。有的时候想做个活动,回馈一下公众号读者,怎么做,又公平又简单呢?现在有些公众号做活动,都是写评论,点赞最多的算中奖。可是呢,你还要提示参与者写快递信息什么的。不优雅,很麻烦。

微信群里,如果抽奖,该怎么弄呢?现在也没看到很好的方式。当然,你可以用发红包,然后选最大红包或是最小红包的方式抽奖,不过,红包小了金额会重复,也不好,不好玩。

公司年会抽奖,怎么搞?不但需要考虑公平性的问题,还要考虑弄这个抽奖活动要花多少成本,我还真见过有的公司为了年会单独写程序抽奖,发短信之类的,无形中多了很多成本,弄不好还当众掉链子。

有没有一个小工具解决这些问题,并且可以用完即走,还灵活优雅呢?我们发布的这个「抽奖助手」小程序或许可以解决上面提到的需求场景。

当然,这个小程序产品还在改进优化中,还可以有很多玩法。懂得运营的用户会发现它的更多场景,比如线下活动,要搞个抽奖活动,只需要把小程序的活动页面打印出来贴那儿就行,就这么简单。


无码科技团队为什么又做小程序了呢?

微信团队对小程序寄予厚望,作为创业团队,也得积极探索可能性。

无码科技团队现在做的几款小程序,除了想触达更多用户,也是看看我们整体的技术协作能力,展示一下产品创新方面的能力。

更重要的是,这个事情很有趣啊。

咋使用?微信里搜索小程序「抽奖助手」即可。

19 | val state: String, //published 20 | val href: String, ///api/posts/33398689 21 | val meta: Meta, 22 | val commentPermission: String, //review 23 | val snapshotUrl: String, 24 | val canComment: Boolean, //false 25 | val slug: Int, //33398689 26 | val commentsCount: Int, //23 27 | val likesCount: Int //27 28 | ) { 29 | data class Links( 30 | val comments: String ///api/posts/33398689/comments 31 | ) 32 | 33 | data class Author( 34 | val bio: String, //「小道消息」,微信号:WebNotes 35 | val isFollowing: Boolean, //false 36 | val hash: String, //99953853cc4219fabe8327301058357c 37 | val uid: Long, //26676083818496 38 | val isOrg: Boolean, //false 39 | val slug: String, //fenng 40 | val isFollowed: Boolean, //false 41 | val description: String, //手艺人.知乎74号用户.微信公众帐号「小道消息」, 微信号: 「WebNotes」https://readhub.me 42 | val name: String, //Fenng 43 | val profileUrl: String, //https://www.zhihu.com/people/fenng 44 | val avatar: Avatar, 45 | val isOrgWhiteList: Boolean, //false 46 | val isBanned: Boolean //false 47 | ) { 48 | data class Avatar( 49 | val id: String, //9ee82f3f07623303aa42164b523ac267 50 | val template: String //https://pic3.zhimg.com/{id}_{size}.jpg 51 | ) 52 | } 53 | 54 | data class Meta( 55 | val previous: Any, //null 56 | val next: Any //null 57 | ) 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/bean/ZhuanlanBean.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.bean 2 | 3 | import android.arch.persistence.room.Entity 4 | import android.arch.persistence.room.PrimaryKey 5 | 6 | 7 | /** 8 | * Created by Meiji on 2018/2/9. 9 | */ 10 | @Entity(tableName = "zhuanlans") 11 | data class ZhuanlanBean( 12 | var followersCount: Int, //28569 13 | var creator: Creator, 14 | var topics: List, 15 | var activateState: String, //activated 16 | var href: String, ///api/columns/design 17 | var acceptSubmission: Boolean, //true 18 | var firstTime: Boolean, //false 19 | // var postTopics: List, 20 | var pendingName: String, 21 | var avatar: Avatar, 22 | var canManage: Boolean, //false 23 | var description: String, //关于用户体验、产品 、技术 和创业的干货集中地 | 最美应用:zuimeia.com 24 | // var pendingTopics: List, 25 | var nameCanEditUntil: Int, //0 26 | var reason: String, 27 | var banUntil: Int, //0 28 | @PrimaryKey var slug: String, //design 29 | var name: String, //可能性 | 产品与大设计 30 | var url: String, ///design 31 | var intro: String, //马力的互联网产品设计与用户体验专栏 32 | var topicsCanEditUntil: Int, //0 33 | var activateAuthorRequested: String, //none 34 | var commentPermission: String, //anyone 35 | var following: Boolean, //false 36 | var postsCount: Int, //200 37 | var canPost: Boolean, //false 38 | var type: Int = 0 39 | ) { 40 | data class Topic( 41 | var url: String, //https://www.zhihu.com/topic/19550517 42 | var id: String, //19550517 43 | var name: String //互联网 44 | ) 45 | 46 | data class PostTopic( 47 | var postsCount: Int, //1 48 | var id: Int, //2 49 | var name: String //知乎 50 | ) 51 | 52 | data class Creator( 53 | var bio: String, //马力在招聘:zhuanlan.zhihu.com/p/31904197 54 | var isFollowing: Boolean, //false 55 | var hash: String, //c6e85ba5d5999df4c5ce2f2903b1ce0e 56 | var uid: Long, //26680571723776 57 | var isOrg: Boolean, //false 58 | var slug: String, //mali 59 | var isFollowed: Boolean, //false 60 | var description: String, //最美应用创始人 产品经理 设计师,欢迎关注微博:@Ma_Li | 他在好奇的注视着这个饶有趣味的世界 | 感谢在评选知乎年度荣誉会员时为我投票的各位,一起认真!| 马力的互联网学习圈:http://mali.brixd.com | 文章索引: https://zhuanlan.zhihu.com/p/25493627 61 | var name: String, //马力 62 | var profileUrl: String, //https://www.zhihu.com/people/mali 63 | var avatar: Avatar, 64 | var isOrgWhiteList: Boolean, //false 65 | var isBanned: Boolean //false 66 | ) { 67 | data class Avatar( 68 | var id: String, //ba332a401 69 | var template: String //https://pic2.zhimg.com/{id}_{size}.jpg 70 | ) 71 | } 72 | 73 | data class Avatar( 74 | var id: String, //v2-5410cdcdc7fb1556a27d0ddc4734e64b 75 | var template: String //https://pic2.zhimg.com/{id}_{size}.jpg 76 | ) 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/binder/FooterViewBinder.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.binder 2 | 3 | import android.graphics.PorterDuff 4 | import android.os.Build 5 | import android.support.v4.graphics.drawable.DrawableCompat 6 | import android.support.v7.widget.RecyclerView 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.ProgressBar 11 | import com.meiji.daily.App 12 | import com.meiji.daily.R 13 | import com.meiji.daily.bean.FooterBean 14 | import com.meiji.daily.util.SettingHelper 15 | import dagger.Lazy 16 | import kotlinx.android.synthetic.main.item_loading.view.* 17 | import me.drakeet.multitype.ItemViewBinder 18 | import javax.inject.Inject 19 | 20 | /** 21 | * Created by Meiji on 2017/6/7. 22 | */ 23 | 24 | internal class FooterViewBinder : ItemViewBinder() { 25 | 26 | @Inject 27 | internal lateinit var mSettingHelper: Lazy 28 | 29 | override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): FooterViewBinder.ViewHolder { 30 | DaggerItemViewComponent.builder() 31 | .appComponent(App.sAppComponent) 32 | .build().inject(this) 33 | val view = inflater.inflate(R.layout.item_loading, parent, false) 34 | return ViewHolder(view) 35 | } 36 | 37 | override fun onBindViewHolder(holder: ViewHolder, item: FooterBean) { 38 | val color = mSettingHelper.get().color 39 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 40 | val wrapDrawable = DrawableCompat.wrap(holder.mProgressBar.indeterminateDrawable) 41 | DrawableCompat.setTint(wrapDrawable, color) 42 | holder.mProgressBar.indeterminateDrawable = DrawableCompat.unwrap(wrapDrawable) 43 | } else { 44 | holder.mProgressBar.indeterminateDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN) 45 | } 46 | } 47 | 48 | inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 49 | 50 | val mProgressBar: ProgressBar = itemView.loading 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/binder/ItemViewComponent.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.binder 2 | 3 | import com.meiji.daily.di.component.AppComponent 4 | import com.meiji.daily.di.scope.FragmentScoped 5 | 6 | import dagger.Component 7 | 8 | /** 9 | * Created by Meiji on 2017/12/28. 10 | */ 11 | @FragmentScoped 12 | @Component(dependencies = arrayOf(AppComponent::class)) 13 | internal interface ItemViewComponent { 14 | 15 | fun inject(footerViewBinder: FooterViewBinder) 16 | 17 | fun inject(zhuanlanViewBinder: ZhuanlanViewBinder) 18 | 19 | fun inject(postsListViewBinder: PostsListViewBinder) 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/binder/PostsListViewBinder.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.binder 2 | 3 | import android.support.v7.widget.CardView 4 | import android.support.v7.widget.RecyclerView 5 | import android.text.TextUtils 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.ImageView 10 | import android.widget.TextView 11 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions 12 | import com.meiji.daily.GlideApp 13 | import com.meiji.daily.R 14 | import com.meiji.daily.bean.PostsListBean 15 | import com.meiji.daily.module.postscontent.PostsContentActivity 16 | import kotlinx.android.synthetic.main.item_postlist.view.* 17 | import me.drakeet.multitype.ItemViewBinder 18 | 19 | /** 20 | * Created by Meiji on 2017/6/6. 21 | */ 22 | 23 | internal class PostsListViewBinder : ItemViewBinder() { 24 | 25 | override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): PostsListViewBinder.ViewHolder { 26 | val view = inflater.inflate(R.layout.item_postlist, parent, false) 27 | return ViewHolder(view) 28 | } 29 | 30 | override fun onBindViewHolder(holder: PostsListViewBinder.ViewHolder, item: PostsListBean) { 31 | val context = holder.itemView.context 32 | val publishedTime = item.publishedTime.substring(0, 10) 33 | val likesCount = item.likesCount.toString() + "赞" 34 | val commentsCount = item.commentsCount.toString() + "条评论" 35 | var titleImage = item.titleImage 36 | val title = item.title 37 | 38 | if (!TextUtils.isEmpty(titleImage)) { 39 | titleImage = item.titleImage.replace("r.jpg", "b.jpg") 40 | GlideApp.with(context) 41 | .load(titleImage) 42 | .centerCrop() 43 | .error(R.color.viewBackground) 44 | .transition(DrawableTransitionOptions().crossFade()) 45 | .into(holder.iv_titleImage) 46 | } else { 47 | holder.iv_titleImage.setImageResource(R.drawable.error_image) 48 | holder.iv_titleImage.scaleType = ImageView.ScaleType.CENTER_CROP 49 | } 50 | holder.tv_publishedTime.text = publishedTime 51 | holder.tv_likesCount.text = likesCount 52 | holder.tv_commentsCount.text = commentsCount 53 | holder.tv_title.text = title 54 | holder.root.setOnClickListener { PostsContentActivity.start(context, item.titleImage, item.title, item.slug.toString()) } 55 | } 56 | 57 | inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 58 | 59 | val root: CardView = itemView.cardview 60 | val tv_publishedTime: TextView = itemView.tv_publishedTime 61 | val tv_likesCount: TextView = itemView.tv_likesCount 62 | val tv_commentsCount: TextView = itemView.tv_commentsCount 63 | val iv_titleImage: ImageView = itemView.iv_titleImage 64 | val tv_title: TextView = itemView.tv_title 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/binder/ZhuanlanViewBinder.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.binder 2 | 3 | import android.support.v7.widget.CardView 4 | import android.support.v7.widget.RecyclerView 5 | import android.text.TextUtils 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.TextView 10 | import com.meiji.daily.GlideApp 11 | import com.meiji.daily.R 12 | import com.meiji.daily.bean.ZhuanlanBean 13 | import com.meiji.daily.module.postslist.PostsListActivity 14 | import de.hdodenhof.circleimageview.CircleImageView 15 | import kotlinx.android.synthetic.main.item_zhuanlan.view.* 16 | import me.drakeet.multitype.ItemViewBinder 17 | 18 | /** 19 | * Created by Meiji on 2017/6/6. 20 | */ 21 | 22 | internal class ZhuanlanViewBinder : ItemViewBinder() { 23 | 24 | override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ZhuanlanViewBinder.ViewHolder { 25 | val view = inflater.inflate(R.layout.item_zhuanlan, parent, false) 26 | return ViewHolder(view) 27 | } 28 | 29 | override fun onBindViewHolder(holder: ZhuanlanViewBinder.ViewHolder, item: ZhuanlanBean) { 30 | val context = holder.itemView.context 31 | val followersCount = item.followersCount.toString() + "人关注TA" 32 | val postsCount = item.postsCount.toString() + "篇文章" 33 | var avatarUrl = item.avatar.template 34 | if (!TextUtils.isEmpty(avatarUrl)) { 35 | // 拼凑avatar链接 36 | avatarUrl = avatarUrl.replace("{id}", item.avatar.id.toString()).replace("{size}", "m") 37 | 38 | GlideApp.with(context) 39 | .load(avatarUrl) 40 | .centerCrop() 41 | .error(R.color.viewBackground) 42 | .into(holder.cv_avatar) 43 | } 44 | holder.tv_name.text = item.name 45 | holder.tv_followersCount.text = followersCount 46 | holder.tv_postsCount.text = postsCount 47 | holder.tv_intro.text = item.intro 48 | holder.cardView.setOnClickListener { PostsListActivity.start(context, item.slug, item.name, item.postsCount) } 49 | } 50 | 51 | inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 52 | 53 | val cardView: CardView = itemView.cardview 54 | val cv_avatar: CircleImageView = itemView.cv_avatar 55 | val tv_name: TextView = itemView.tv_name 56 | val tv_followersCount: TextView = itemView.tv_followersCount 57 | val tv_postsCount: TextView = itemView.tv_postsCount 58 | val tv_intro: TextView = itemView.tv_intro 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/data/local/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.data.local 2 | 3 | import android.arch.persistence.room.Database 4 | import android.arch.persistence.room.Room 5 | import android.arch.persistence.room.RoomDatabase 6 | import android.arch.persistence.room.TypeConverters 7 | import android.content.Context 8 | import android.support.annotation.VisibleForTesting 9 | 10 | import com.meiji.daily.bean.ZhuanlanBean 11 | import com.meiji.daily.data.local.converter.ZhuanlanBeanConverter 12 | import com.meiji.daily.data.local.dao.ZhuanlanDao 13 | 14 | /** 15 | * Created by Meiji on 2017/11/28. 16 | */ 17 | @Database(entities = [(ZhuanlanBean::class)], version = 1, exportSchema = false) 18 | @TypeConverters(ZhuanlanBeanConverter::class) 19 | abstract class AppDatabase : RoomDatabase() { 20 | 21 | abstract fun zhuanlanDao(): ZhuanlanDao 22 | 23 | companion object { 24 | 25 | @VisibleForTesting 26 | val DATABASE_NAME = "Daily" 27 | @Volatile 28 | private var sInstance: AppDatabase? = null 29 | 30 | fun getInstance(context: Context): AppDatabase { 31 | if (sInstance == null) { 32 | synchronized(AppDatabase::class.java) { 33 | if (sInstance == null) { 34 | sInstance = Room.databaseBuilder(context, 35 | AppDatabase::class.java, 36 | DATABASE_NAME 37 | ).build() 38 | } 39 | } 40 | } 41 | return sInstance!! 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/data/local/converter/ZhuanlanBeanConverter.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.data.local.converter 2 | 3 | import android.arch.persistence.room.TypeConverter 4 | import com.meiji.daily.bean.ZhuanlanBean 5 | import com.squareup.moshi.JsonAdapter 6 | import com.squareup.moshi.Moshi 7 | import com.squareup.moshi.Types 8 | 9 | /** 10 | * Created by Meiji on 2017/11/28. 11 | */ 12 | 13 | class ZhuanlanBeanConverter { 14 | 15 | private val mMoshi = Moshi.Builder().build() 16 | private val mCreatorAdapter: JsonAdapter 17 | private var mAvatarAdapter: JsonAdapter 18 | private var mTopicAdapter: JsonAdapter> 19 | 20 | init { 21 | mCreatorAdapter = mMoshi.adapter(ZhuanlanBean.Creator::class.java) 22 | mAvatarAdapter = mMoshi.adapter(ZhuanlanBean.Avatar::class.java) 23 | 24 | val type = Types.newParameterizedType(List::class.java, ZhuanlanBean.Topic::class.java) 25 | mTopicAdapter = mMoshi.adapter>(type) 26 | } 27 | 28 | @TypeConverter 29 | fun fromCreator(c: ZhuanlanBean.Creator): String = mCreatorAdapter.toJson(c) 30 | 31 | @TypeConverter 32 | fun toCreator(s: String): ZhuanlanBean.Creator = mCreatorAdapter.fromJson(s)!! 33 | 34 | @TypeConverter 35 | fun fromTopicList(l: List): String = mTopicAdapter.toJson(l) 36 | 37 | @TypeConverter 38 | fun toTopicList(s: String): List = mTopicAdapter.fromJson(s)!! 39 | 40 | @TypeConverter 41 | fun fromAvatar(a: ZhuanlanBean.Avatar): String = mAvatarAdapter.toJson(a) 42 | 43 | @TypeConverter 44 | fun toAvatar(s: String): ZhuanlanBean.Avatar = mAvatarAdapter.fromJson(s)!! 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/data/local/dao/ZhuanlanDao.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.data.local.dao 2 | 3 | /** 4 | * Created by Meiji on 2017/11/28. 5 | */ 6 | 7 | import android.arch.persistence.room.Dao 8 | import android.arch.persistence.room.Insert 9 | import android.arch.persistence.room.OnConflictStrategy 10 | import android.arch.persistence.room.Query 11 | 12 | import com.meiji.daily.bean.ZhuanlanBean 13 | 14 | import io.reactivex.Maybe 15 | 16 | @Dao 17 | interface ZhuanlanDao { 18 | 19 | @Insert(onConflict = OnConflictStrategy.IGNORE) 20 | fun insert(zhuanlanBean: ZhuanlanBean): Long 21 | 22 | @Insert(onConflict = OnConflictStrategy.IGNORE) 23 | fun insert(list: MutableList) 24 | 25 | @Query("SELECT * FROM zhuanlans WHERE type = :type") 26 | fun query(type: Int): Maybe> 27 | 28 | @Query("DELETE FROM zhuanlans WHERE slug = :slug") 29 | fun delete(slug: String) 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/data/remote/IApi.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.data.remote 2 | 3 | import com.meiji.daily.bean.PostsContentBean 4 | import com.meiji.daily.bean.PostsListBean 5 | import com.meiji.daily.bean.ZhuanlanBean 6 | 7 | import io.reactivex.Maybe 8 | import retrofit2.http.GET 9 | import retrofit2.http.Path 10 | import retrofit2.http.Query 11 | 12 | /** 13 | * Created by Meiji on 2016/11/16. 14 | */ 15 | 16 | interface IApi { 17 | 18 | /** 19 | * 获取专栏信息 Retrofit + RxJava 20 | * https://zhuanlan.zhihu.com/api/columns/design 21 | * 22 | * @param slug 23 | * @return 24 | */ 25 | @GET("api/columns/{slug}") 26 | fun getZhuanlanBean(@Path("slug") slug: String): Maybe 27 | 28 | /** 29 | * 获取专栏文章 Retrofit + RxJava 30 | * https://zhuanlan.zhihu.com/api/columns/design/posts?limit=10&offset=10 31 | * 32 | * @param slug 专栏ID 33 | * @param offset 偏移量 34 | * @return 35 | */ 36 | @GET("api/columns/{slug}/posts?limit=10") 37 | fun getPostsList( 38 | @Path("slug") slug: String, 39 | @Query("offset") offset: Int): Maybe> 40 | 41 | /** 42 | * 获取文章内容 Retrofit + RxJava 43 | * https://zhuanlan.zhihu.com/api/posts/25982605 44 | * 45 | * @param slug 文章ID 46 | * @return 47 | */ 48 | @GET("api/posts/{slug}") 49 | fun getPostsContentBean(@Path("slug") slug: String): Maybe 50 | 51 | companion object { 52 | 53 | const val COLUMN_URL = "https://zhuanlan.zhihu.com/api/columns/" 54 | 55 | const val POST_URL = "https://zhuanlan.zhihu.com/api/posts/" 56 | 57 | const val API_BASE = "https://zhuanlan.zhihu.com/" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/di/component/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.di.component 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import com.meiji.daily.App 7 | import com.meiji.daily.data.local.AppDatabase 8 | import com.meiji.daily.di.module.AppModule 9 | import com.meiji.daily.util.RxBusHelper 10 | import com.meiji.daily.util.SettingHelper 11 | import dagger.BindsInstance 12 | import dagger.Component 13 | import retrofit2.Retrofit 14 | import javax.inject.Named 15 | import javax.inject.Singleton 16 | 17 | /** 18 | * Created by Meiji on 2017/12/21. 19 | */ 20 | @Singleton 21 | @Component(modules = arrayOf(AppModule::class)) 22 | interface AppComponent { 23 | 24 | @get:Named("application") 25 | val application: Application 26 | 27 | @get:Named("context") 28 | val context: Context 29 | 30 | val appDatabase: AppDatabase 31 | 32 | val settingHelper: SettingHelper 33 | 34 | val retrofit: Retrofit 35 | 36 | val sharedPreferences: SharedPreferences 37 | 38 | val rxBus: RxBusHelper 39 | 40 | fun inject(app: App) 41 | 42 | @Component.Builder 43 | interface Builder { 44 | @BindsInstance 45 | fun application(application: Application): Builder 46 | 47 | @BindsInstance 48 | fun context(context: Context): Builder 49 | 50 | fun build(): AppComponent 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/di/component/CommonActivityComponent.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.di.component 2 | 3 | import com.meiji.daily.MainActivity 4 | import com.meiji.daily.di.scope.ActivityScoped 5 | import com.meiji.daily.module.base.BaseActivity 6 | 7 | import dagger.Component 8 | 9 | /** 10 | * Created by Meiji on 2017/12/28. 11 | */ 12 | 13 | @ActivityScoped 14 | @Component(dependencies = arrayOf(AppComponent::class)) 15 | interface CommonActivityComponent { 16 | 17 | fun inject(baseActivity: BaseActivity) 18 | 19 | fun inject(mainActivity: MainActivity) 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/di/module/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.di.module 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import android.preference.PreferenceManager 7 | import com.meiji.daily.data.local.AppDatabase 8 | import com.meiji.daily.util.RetrofitHelper 9 | import dagger.Module 10 | import dagger.Provides 11 | import retrofit2.Retrofit 12 | import javax.inject.Named 13 | import javax.inject.Singleton 14 | 15 | /** 16 | * Created by Meiji on 2017/12/21. 17 | */ 18 | @Module 19 | class AppModule { 20 | 21 | @Named("context") 22 | @Provides 23 | @Singleton 24 | internal fun provideContext(context: Context): Context { 25 | return context 26 | } 27 | 28 | @Named("application") 29 | @Provides 30 | @Singleton 31 | internal fun provideApplication(application: Application): Application { 32 | return application 33 | } 34 | 35 | @Singleton 36 | @Provides 37 | internal fun provideAppDatabase(context: Context): AppDatabase { 38 | return AppDatabase.getInstance(context) 39 | } 40 | 41 | @Singleton 42 | @Provides 43 | internal fun provideSharedPreferences(context: Context): SharedPreferences { 44 | return PreferenceManager.getDefaultSharedPreferences(context) 45 | } 46 | 47 | @Singleton 48 | @Provides 49 | internal fun provideRetrofit(context: Context): Retrofit { 50 | return RetrofitHelper(context).retrofit 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/di/scope/ActivityScoped.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.di.scope 2 | 3 | import com.meiji.daily.di.component.AppComponent 4 | 5 | import java.lang.annotation.Documented 6 | import java.lang.annotation.Retention 7 | import java.lang.annotation.RetentionPolicy 8 | 9 | import javax.inject.Scope 10 | 11 | /** 12 | * In Dagger, an unscoped component cannot depend on a scoped component. As 13 | * [AppComponent] is a scoped component (`@Singleton`, we create a custom 14 | * scope to be used by all fragment components. Additionally, a component with a specific scope 15 | * cannot have a sub component with the same scope. 16 | */ 17 | @Documented 18 | @Scope 19 | @Retention(RetentionPolicy.RUNTIME) 20 | annotation class ActivityScoped 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/di/scope/FragmentScoped.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.di.scope 2 | 3 | import java.lang.annotation.Retention 4 | import java.lang.annotation.RetentionPolicy 5 | 6 | import javax.inject.Scope 7 | 8 | @Scope 9 | @Retention(RetentionPolicy.RUNTIME) 10 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FILE, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) 11 | annotation class FragmentScoped 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/listener/IOnItemClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.listener 2 | 3 | import android.view.View 4 | 5 | /** 6 | * Created by Meiji on 2016/11/18. 7 | */ 8 | 9 | interface IOnItemClickListener { 10 | 11 | /** 12 | * RecyclerView Item点击事件 13 | * 14 | * @param view 15 | * @param position 16 | */ 17 | fun onClick(view: View, position: Int) 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.base 2 | 3 | import android.graphics.drawable.ColorDrawable 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.support.v7.app.AppCompatActivity 7 | import android.support.v7.widget.Toolbar 8 | import android.view.MenuItem 9 | 10 | import com.afollestad.materialdialogs.color.CircleView 11 | import com.meiji.daily.App 12 | import com.meiji.daily.R 13 | import com.meiji.daily.di.component.DaggerCommonActivityComponent 14 | import com.meiji.daily.util.SettingHelper 15 | 16 | import javax.inject.Inject 17 | 18 | /** 19 | * Created by Meiji on 2017/12/5. 20 | */ 21 | 22 | abstract class BaseActivity : AppCompatActivity() { 23 | 24 | @Inject 25 | lateinit var mSettingHelper: SettingHelper 26 | 27 | /** 28 | * 绑定布局文件 29 | * 30 | * @return 布局文件ID 31 | */ 32 | protected abstract fun attachLayoutId(): Int 33 | 34 | /** 35 | * 初始化视图控件 36 | */ 37 | protected abstract fun initViews() 38 | 39 | /** 40 | * 初始化数据 41 | * 42 | * @param savedInstanceState 43 | */ 44 | protected open fun initData(savedInstanceState: Bundle?) {} 45 | 46 | protected fun initTheme() { 47 | val isNightMode = mSettingHelper.isNightMode 48 | if (isNightMode) { 49 | setTheme(R.style.DarkTheme) 50 | } else { 51 | setTheme(R.style.LightTheme) 52 | } 53 | } 54 | 55 | /** 56 | * 初始化 Toolbar 57 | * 58 | * @param toolbar 59 | * @param homeAsUpEnabled 60 | * @param title 61 | */ 62 | fun initToolBar(toolbar: Toolbar, homeAsUpEnabled: Boolean, title: String?) { 63 | toolbar.title = title 64 | setSupportActionBar(toolbar) 65 | if (supportActionBar != null) { 66 | supportActionBar!!.setDisplayHomeAsUpEnabled(homeAsUpEnabled) 67 | } 68 | } 69 | 70 | override fun onCreate(savedInstanceState: Bundle?) { 71 | DaggerCommonActivityComponent.builder() 72 | .appComponent(App.sAppComponent) 73 | .build().inject(this) 74 | super.onCreate(savedInstanceState) 75 | initTheme() 76 | setContentView(attachLayoutId()) 77 | initViews() 78 | initData(savedInstanceState) 79 | } 80 | 81 | override fun onResume() { 82 | super.onResume() 83 | val color = mSettingHelper.color 84 | if (supportActionBar != null) 85 | supportActionBar!!.setBackgroundDrawable(ColorDrawable(color)) 86 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 87 | window.statusBarColor = CircleView.shiftColorDown(color) 88 | window.navigationBarColor = color 89 | } 90 | } 91 | 92 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 93 | val id = item.itemId 94 | if (id == android.R.id.home) { 95 | onBackPressed() 96 | } 97 | return super.onOptionsItemSelected(item) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.base 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.Fragment 5 | import android.support.v7.widget.Toolbar 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | 10 | /** 11 | * Created by Meiji on 2017/12/4. 12 | */ 13 | 14 | abstract class BaseFragment : Fragment() { 15 | 16 | /** 17 | * 绑定布局文件 18 | * 19 | * @return 布局文件ID 20 | */ 21 | protected abstract fun attachLayoutId(): Int 22 | 23 | /** 24 | * 初始化视图控件 25 | */ 26 | protected abstract fun initViews(view: View) 27 | 28 | /** 29 | * 初始化数据 30 | */ 31 | protected fun initData() {} 32 | 33 | /** 34 | * 订阅UI组件 35 | */ 36 | protected open fun subscribeUI() {} 37 | 38 | /** 39 | * 初始化 Dagger 40 | */ 41 | protected open fun initInject() {} 42 | 43 | /** 44 | * 初始化 Toolbar 45 | * 46 | * @param toolbar 47 | * @param homeAsUpEnabled 48 | * @param title 49 | */ 50 | protected fun initToolBar(toolbar: Toolbar, homeAsUpEnabled: Boolean, title: String) { 51 | if (activity != null) { 52 | (activity as BaseActivity).initToolBar(toolbar, homeAsUpEnabled, title) 53 | } 54 | } 55 | 56 | override fun onCreate(savedInstanceState: Bundle?) { 57 | super.onCreate(savedInstanceState) 58 | initInject() 59 | } 60 | 61 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 62 | val view = inflater.inflate(attachLayoutId(), container, false) 63 | initViews(view) 64 | initData() 65 | return view 66 | } 67 | 68 | override fun onActivityCreated(savedInstanceState: Bundle?) { 69 | super.onActivityCreated(savedInstanceState) 70 | if (!isAdded) { 71 | return 72 | } 73 | subscribeUI() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/postscontent/PostsContentActivity.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.postscontent 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | 7 | import com.meiji.daily.R 8 | import com.meiji.daily.module.base.BaseActivity 9 | 10 | /** 11 | * Created by Meiji on 2017/12/6. 12 | */ 13 | 14 | class PostsContentActivity : BaseActivity() { 15 | 16 | override fun attachLayoutId() = R.layout.container 17 | 18 | override fun initViews() { 19 | 20 | } 21 | 22 | override fun initData(savedInstanceState: Bundle?) { 23 | if (intent == null) { 24 | finish() 25 | return 26 | } 27 | val titleImage = intent.getStringExtra(EXTRA_TITLEIMAGE) 28 | val title = intent.getStringExtra(EXTRA_TITLE) 29 | val slug = intent.getStringExtra(EXTRA_SLUG) 30 | // if (supportActionBar != null) { 31 | // supportActionBar!!.title = title 32 | // } 33 | if (savedInstanceState == null) { 34 | supportFragmentManager 35 | .beginTransaction() 36 | .replace(R.id.container, PostsContentView.newInstance(titleImage, title, slug)) 37 | .commit() 38 | } 39 | } 40 | 41 | companion object { 42 | 43 | internal val EXTRA_TITLEIMAGE = "EXTRA_TITLEIMAGE" 44 | internal val EXTRA_TITLE = "EXTRA_TITLE" 45 | internal val EXTRA_SLUG = "EXTRA_SLUG" 46 | 47 | fun start(context: Context, titleImage: String, title: String, slug: String) { 48 | val starter = Intent(context, PostsContentActivity::class.java) 49 | starter.putExtra(EXTRA_TITLEIMAGE, titleImage) 50 | starter.putExtra(EXTRA_TITLE, title) 51 | starter.putExtra(EXTRA_SLUG, slug) 52 | context.startActivity(starter) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/postscontent/PostsContentComponent.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.postscontent 2 | 3 | import com.meiji.daily.di.component.AppComponent 4 | import com.meiji.daily.di.scope.FragmentScoped 5 | 6 | import dagger.Component 7 | 8 | /** 9 | * Created by Meiji on 2017/12/28. 10 | */ 11 | 12 | @FragmentScoped 13 | @Component(modules = [(PostsContentModule::class)], dependencies = [(AppComponent::class)]) 14 | interface PostsContentComponent { 15 | 16 | fun inject(postsContentView: PostsContentView) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/postscontent/PostsContentModule.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.postscontent 2 | 3 | import android.app.Application 4 | import android.arch.lifecycle.ViewModelProviders 5 | import com.meiji.daily.di.scope.FragmentScoped 6 | import dagger.Module 7 | import dagger.Provides 8 | import retrofit2.Retrofit 9 | import javax.inject.Named 10 | 11 | /** 12 | * Created by Meiji on 2017/12/28. 13 | */ 14 | 15 | @Module 16 | class PostsContentModule(private val mPostsContentView: PostsContentView) { 17 | 18 | @FragmentScoped 19 | @Provides 20 | internal fun provideModel(@Named("application") application: Application, 21 | @Named("slug") slug: String, 22 | retrofit: Retrofit): PostsContentViewModel { 23 | val factory = PostsContentViewModel.Factory(application, slug, retrofit) 24 | return ViewModelProviders.of(mPostsContentView, factory).get(PostsContentViewModel::class.java) 25 | } 26 | 27 | @Provides 28 | @FragmentScoped 29 | @Named("image") 30 | internal fun provideImage(): String { 31 | val bundle = mPostsContentView.arguments 32 | return if (bundle != null) { 33 | bundle.getString(PostsContentView.ARGUMENT_TITLEIMAGE) 34 | } else "" 35 | } 36 | 37 | @Provides 38 | @FragmentScoped 39 | @Named("title") 40 | internal fun provideTitle(): String { 41 | val bundle = mPostsContentView.arguments 42 | return if (bundle != null) { 43 | bundle.getString(PostsContentView.ARGUMENT_TITLE) 44 | } else "" 45 | } 46 | 47 | @Provides 48 | @FragmentScoped 49 | @Named("slug") 50 | internal fun provideSlug(): String { 51 | val bundle = mPostsContentView.arguments 52 | return bundle?.getString(PostsContentView.ARGUMENT_SLUG, "") ?: "" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/postscontent/PostsContentView.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.postscontent 2 | 3 | import android.annotation.SuppressLint 4 | import android.arch.lifecycle.Observer 5 | import android.content.Intent 6 | import android.content.res.ColorStateList 7 | import android.os.Bundle 8 | import android.support.design.widget.Snackbar 9 | import android.text.TextUtils 10 | import android.view.KeyEvent 11 | import android.view.View 12 | import android.webkit.WebSettings 13 | import android.webkit.WebView 14 | import android.webkit.WebViewClient 15 | import android.widget.ImageView 16 | import com.afollestad.materialdialogs.MaterialDialog 17 | import com.afollestad.materialdialogs.Theme 18 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions 19 | import com.meiji.daily.App 20 | import com.meiji.daily.GlideApp 21 | import com.meiji.daily.R 22 | import com.meiji.daily.data.remote.IApi 23 | import com.meiji.daily.module.base.BaseFragment 24 | import com.meiji.daily.util.SettingHelper 25 | import kotlinx.android.synthetic.main.activity_postscontent.view.* 26 | import javax.inject.Inject 27 | import javax.inject.Named 28 | 29 | /** 30 | * Created by Meiji on 2017/12/6. 31 | */ 32 | 33 | class PostsContentView : BaseFragment() { 34 | 35 | @field:[Inject Named("image")] 36 | lateinit var mImage: String 37 | 38 | @field:[Inject Named("title")] 39 | lateinit var mTitle: String 40 | 41 | @field:[Inject Named("slug")] 42 | lateinit var mSlug: String 43 | 44 | @Inject 45 | lateinit var mModel: PostsContentViewModel 46 | @Inject 47 | lateinit var mSettingHelper: SettingHelper 48 | 49 | private lateinit var mWebView: WebView 50 | private var mDialog: MaterialDialog? = null 51 | 52 | override fun initInject() { 53 | DaggerPostsContentComponent.builder() 54 | .appComponent(App.sAppComponent) 55 | .postsContentModule(com.meiji.daily.module.postscontent.PostsContentModule(this)) 56 | .build().inject(this) 57 | } 58 | 59 | fun onSetWebView(url: String?) { 60 | mWebView.loadDataWithBaseURL(null, url, "text/html", "utf-8", null) 61 | } 62 | 63 | @SuppressLint("SetJavaScriptEnabled") 64 | private fun initWebClient() { 65 | val settings = mWebView.settings 66 | settings.javaScriptEnabled = true 67 | // 缩放,设置为不能缩放可以防止页面上出现放大和缩小的图标 68 | settings.builtInZoomControls = false 69 | // 缓存 70 | settings.cacheMode = WebSettings.LOAD_DEFAULT 71 | // 开启DOM storage API功能 72 | settings.domStorageEnabled = true 73 | // 开启application Cache功能 74 | settings.setAppCacheEnabled(false) 75 | // 不调用第三方浏览器即可进行页面反应 76 | mWebView.webViewClient = object : WebViewClient() { 77 | override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { 78 | view.loadUrl(url) 79 | return true 80 | } 81 | } 82 | 83 | mWebView.setOnKeyListener(View.OnKeyListener { view, i, keyEvent -> 84 | if (keyEvent.keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) { 85 | mWebView.goBack() 86 | return@OnKeyListener true 87 | } 88 | false 89 | }) 90 | } 91 | 92 | override fun attachLayoutId() = R.layout.activity_postscontent 93 | 94 | override fun initViews(view: View) { 95 | mWebView = view.webview_content 96 | 97 | initToolBar(view.toolbar_title, true, mTitle) 98 | 99 | view.toolbar_title.setOnClickListener { view.scrollView.smoothScrollTo(0, 0) } 100 | 101 | view.fab_share.backgroundTintList = ColorStateList.valueOf(mSettingHelper.color) 102 | view.fab_share.setOnClickListener { 103 | val shareIntent = Intent().also { it.action = Intent.ACTION_SEND; it.type = "text/plain" } 104 | val shareText = mTitle + " " + IApi.POST_URL + mSlug 105 | shareIntent.putExtra(Intent.EXTRA_TEXT, shareText) 106 | startActivity(Intent.createChooser(shareIntent, getString(R.string.share_to))) 107 | } 108 | 109 | view.collapsing_layout.let { 110 | it.setExpandedTitleTextAppearance(R.style.ExpandedAppBar) 111 | it.setCollapsedTitleTextAppearance(R.style.CollapsedAppBar) 112 | it.title = mTitle 113 | } 114 | 115 | mDialog = MaterialDialog.Builder(context!!) 116 | .progress(true, 0) 117 | .content(R.string.md_loading) 118 | .theme(if (mSettingHelper.isNightMode) Theme.DARK else Theme.LIGHT) 119 | .cancelable(true) 120 | .build() 121 | 122 | if (TextUtils.isEmpty(mImage)) { 123 | view.iv_titleimage.setImageResource(R.drawable.error_image) 124 | view.iv_titleimage.scaleType = ImageView.ScaleType.CENTER_CROP 125 | } else { 126 | GlideApp.with(this) 127 | .load(mImage) 128 | .centerCrop() 129 | .error(R.color.viewBackground) 130 | .transition(DrawableTransitionOptions().crossFade()) 131 | .into(view.iv_titleimage) 132 | } 133 | 134 | initWebClient() 135 | } 136 | 137 | override fun subscribeUI() { 138 | mModel.html.observe(this, Observer { s -> 139 | if (!TextUtils.isEmpty(s)) { 140 | onSetWebView(s) 141 | } else { 142 | onShowNetError() 143 | } 144 | }) 145 | mModel.isLoading.observe(this, Observer { aBoolean -> 146 | if (aBoolean!!) { 147 | onShowLoading() 148 | } else { 149 | onHideLoading() 150 | } 151 | }) 152 | } 153 | 154 | fun onShowLoading() { 155 | mDialog!!.show() 156 | } 157 | 158 | fun onHideLoading() { 159 | mDialog!!.dismiss() 160 | } 161 | 162 | fun onShowNetError() { 163 | mDialog!!.dismiss() 164 | Snackbar.make(mWebView, R.string.network_error, Snackbar.LENGTH_SHORT).show() 165 | } 166 | 167 | companion object { 168 | 169 | internal val ARGUMENT_TITLEIMAGE = "ARGUMENT_TITLEIMAGE" 170 | internal val ARGUMENT_TITLE = "ARGUMENT_TITLE" 171 | internal val ARGUMENT_SLUG = "ARGUMENT_SLUG" 172 | 173 | fun newInstance(titleImage: String, title: String, slug: String): PostsContentView { 174 | val args = Bundle() 175 | args.putString(ARGUMENT_TITLEIMAGE, titleImage) 176 | args.putString(ARGUMENT_TITLE, title) 177 | args.putString(ARGUMENT_SLUG, slug) 178 | val fragment = com.meiji.daily.module.postscontent.PostsContentView() 179 | fragment.arguments = args 180 | return fragment 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/postscontent/PostsContentViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.postscontent 2 | 3 | import android.app.Application 4 | import android.arch.lifecycle.AndroidViewModel 5 | import android.arch.lifecycle.MutableLiveData 6 | import android.arch.lifecycle.ViewModel 7 | import android.arch.lifecycle.ViewModelProvider 8 | import com.meiji.daily.data.remote.IApi 9 | import com.meiji.daily.io 10 | import com.meiji.daily.mainThread 11 | import com.meiji.daily.util.ErrorAction 12 | import io.reactivex.disposables.CompositeDisposable 13 | import io.reactivex.functions.Consumer 14 | import retrofit2.Retrofit 15 | 16 | /** 17 | * Created by Meiji on 2017/12/5. 18 | */ 19 | 20 | class PostsContentViewModel 21 | internal constructor(application: Application, 22 | slug: String, 23 | private val mRetrofit: Retrofit) : AndroidViewModel(application) { 24 | private val mDisposable: CompositeDisposable 25 | 26 | var isLoading: MutableLiveData 27 | private set 28 | var html: MutableLiveData 29 | private set 30 | 31 | init { 32 | isLoading = MutableLiveData() 33 | html = MutableLiveData() 34 | mDisposable = CompositeDisposable() 35 | handleData(slug) 36 | } 37 | 38 | private fun handleData(slug: String) { 39 | isLoading.value = true 40 | 41 | mRetrofit.create(IApi::class.java).getPostsContentBean(slug) 42 | .subscribeOn(io) 43 | .observeOn(mainThread) 44 | .subscribe(Consumer { bean -> 45 | isLoading.value = false 46 | html.value = parserHTML(bean.content) 47 | }, object : ErrorAction() { 48 | override fun doAction() { 49 | isLoading.value = false 50 | html.value = null 51 | } 52 | }.action()).let { mDisposable.add(it) } 53 | } 54 | 55 | private fun parserHTML(content: String): String { 56 | val css = "" 57 | 58 | return ("\n" 59 | + "\n" 60 | + "\n" 61 | + "\t\n\n" 62 | + css 63 | + "\n" 64 | + content 65 | + "\n") 66 | } 67 | 68 | override fun onCleared() { 69 | mDisposable.clear() 70 | super.onCleared() 71 | } 72 | 73 | class Factory internal constructor(private val mApplication: Application, 74 | private val mSlug: String, 75 | private val mRetrofit: Retrofit) : ViewModelProvider.NewInstanceFactory() { 76 | 77 | override fun create(modelClass: Class): T { 78 | return com.meiji.daily.module.postscontent.PostsContentViewModel(mApplication, mSlug, mRetrofit) as T 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/postslist/PostsListActivity.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.postslist 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | 7 | import com.meiji.daily.R 8 | import com.meiji.daily.module.base.BaseActivity 9 | 10 | /** 11 | * Created by Meiji on 2017/12/5. 12 | */ 13 | 14 | class PostsListActivity : BaseActivity() { 15 | 16 | override fun attachLayoutId() = R.layout.container 17 | 18 | override fun initViews() {} 19 | 20 | override fun initData(savedInstanceState: Bundle?) { 21 | if (intent == null) { 22 | finish() 23 | return 24 | } 25 | val slug = intent.getStringExtra(EXTRA_SLUG) 26 | val title = intent.getStringExtra(EXTRA_NAME) 27 | val postCount = intent.getIntExtra(EXTRA_POSTSCOUNT, 0) 28 | // supportActionBar?.title = title 29 | if (savedInstanceState == null) { 30 | supportFragmentManager 31 | .beginTransaction() 32 | .replace(R.id.container, PostsListView.newInstance(slug, title, postCount)) 33 | .commit() 34 | } 35 | } 36 | 37 | companion object { 38 | 39 | private val EXTRA_SLUG = "EXTRA_SLUG" 40 | private val EXTRA_NAME = "EXTRA_NAME" 41 | private val EXTRA_POSTSCOUNT = "EXTRA_POSTSCOUNT" 42 | 43 | fun start(context: Context, slug: String, title: String, postsCount: Int) { 44 | val starter = Intent(context, PostsListActivity::class.java) 45 | .putExtra(EXTRA_SLUG, slug) 46 | .putExtra(EXTRA_NAME, title) 47 | .putExtra(EXTRA_POSTSCOUNT, postsCount) 48 | context.startActivity(starter) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/postslist/PostsListComponent.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.postslist 2 | 3 | import com.meiji.daily.di.component.AppComponent 4 | import com.meiji.daily.di.scope.FragmentScoped 5 | 6 | import dagger.Component 7 | 8 | /** 9 | * Created by Meiji on 2017/12/28. 10 | */ 11 | @FragmentScoped 12 | @Component(modules = [(PostsListModule::class)], dependencies = [(AppComponent::class)]) 13 | interface PostsListComponent { 14 | 15 | fun inject(postsListView: PostsListView) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/postslist/PostsListModule.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.postslist 2 | 3 | import android.app.Application 4 | import android.arch.lifecycle.ViewModelProviders 5 | import com.meiji.daily.di.scope.FragmentScoped 6 | import dagger.Module 7 | import dagger.Provides 8 | import retrofit2.Retrofit 9 | import javax.inject.Named 10 | 11 | /** 12 | * Created by Meiji on 2017/12/28. 13 | */ 14 | 15 | @Module 16 | class PostsListModule(private val mPostsListView: PostsListView) { 17 | 18 | @FragmentScoped 19 | @Provides 20 | internal fun provideModule(@Named("application") application: Application, 21 | @Named("slug") slug: String, 22 | @Named("count") postCount: Int, 23 | retrofit: Retrofit): PostsListViewModel { 24 | val factory = PostsListViewModel.Factory(application, slug, postCount, retrofit) 25 | return ViewModelProviders.of(mPostsListView, factory).get(PostsListViewModel::class.java) 26 | } 27 | 28 | @FragmentScoped 29 | @Provides 30 | @Named("slug") 31 | internal fun provideSlug(): String { 32 | val bundle = mPostsListView.arguments 33 | return if (bundle != null) { 34 | bundle.getString(PostsListView.ARGUMENT_SLUG) 35 | } else "" 36 | } 37 | 38 | @FragmentScoped 39 | @Provides 40 | @Named("count") 41 | internal fun providePostCount(): Int { 42 | val bundle = mPostsListView.arguments 43 | return bundle?.getInt(PostsListView.ARGUMENT_POSTSCOUNT, 0) ?: 0 44 | } 45 | 46 | @FragmentScoped 47 | @Provides 48 | @Named("title") 49 | internal fun provideTitle(): String { 50 | val bundle = mPostsListView.arguments 51 | return if (bundle != null) { 52 | bundle.getString(PostsListView.ARGUMENT_NAME) 53 | } else "" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/postslist/PostsListView.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.postslist 2 | 3 | import android.arch.lifecycle.Observer 4 | import android.os.Bundle 5 | import android.support.design.widget.Snackbar 6 | import android.support.v4.widget.SwipeRefreshLayout 7 | import android.support.v7.widget.LinearLayoutManager 8 | import android.view.View 9 | import com.meiji.daily.App 10 | import com.meiji.daily.R 11 | import com.meiji.daily.bean.FooterBean 12 | import com.meiji.daily.bean.PostsListBean 13 | import com.meiji.daily.binder.FooterViewBinder 14 | import com.meiji.daily.binder.PostsListViewBinder 15 | import com.meiji.daily.module.base.BaseFragment 16 | import com.meiji.daily.module.postslist.PostsListModule 17 | import com.meiji.daily.util.DiffCallback 18 | import com.meiji.daily.util.OnLoadMoreListener 19 | import com.meiji.daily.util.SettingHelper 20 | import kotlinx.android.synthetic.main.activity_postslist.* 21 | import kotlinx.android.synthetic.main.activity_postslist.view.* 22 | import me.drakeet.multitype.Items 23 | import me.drakeet.multitype.MultiTypeAdapter 24 | import javax.inject.Inject 25 | import javax.inject.Named 26 | 27 | 28 | /** 29 | * Created by Meiji on 2017/12/5. 30 | */ 31 | 32 | class PostsListView : BaseFragment(), SwipeRefreshLayout.OnRefreshListener { 33 | 34 | @field:[Inject Named("title")] 35 | lateinit var mTitle: String 36 | @Inject 37 | lateinit var mModel: PostsListViewModel 38 | @Inject 39 | lateinit var mSettingHelper: SettingHelper 40 | 41 | private val mOldItems = Items() 42 | private var mAdapter: MultiTypeAdapter? = null 43 | private var mCanloadmore: Boolean = false 44 | 45 | override fun initInject() { 46 | DaggerPostsListComponent.builder() 47 | .appComponent(App.sAppComponent) 48 | .postsListModule(PostsListModule(this)) 49 | .build().inject(this) 50 | } 51 | 52 | override fun attachLayoutId() = R.layout.activity_postslist 53 | 54 | override fun initViews(view: View) { 55 | initToolBar(view.toolbar_title, true, mTitle) 56 | view.toolbar_title.setOnClickListener { view.recycler_view.smoothScrollToPosition(0) } 57 | view.recycler_view.layoutManager = LinearLayoutManager(context) 58 | view.recycler_view.setHasFixedSize(true) 59 | // 设置下拉刷新的按钮的颜色 60 | view.refresh_layout.setColorSchemeColors(mSettingHelper.color) 61 | view.refresh_layout.setOnRefreshListener(this) 62 | 63 | mAdapter = MultiTypeAdapter() 64 | mAdapter!!.register(PostsListBean::class.java, PostsListViewBinder()) 65 | mAdapter!!.register(FooterBean::class.java, FooterViewBinder()) 66 | mAdapter!!.items = mOldItems 67 | view.recycler_view.adapter = mAdapter 68 | } 69 | 70 | override fun subscribeUI() { 71 | mModel.mListLiveData.observe(this, Observer> { list -> 72 | if (null != list && list.isNotEmpty()) { 73 | onSetAdapter(list) 74 | } else { 75 | onShowNetError() 76 | } 77 | }) 78 | mModel.isLoading.observe(this, Observer { aBoolean -> 79 | if (aBoolean!!) { 80 | onShowLoading() 81 | } else { 82 | onHideLoading() 83 | } 84 | }) 85 | mModel.isEnd.observe(this, Observer { 86 | Snackbar.make(refresh_layout, R.string.no_more, Snackbar.LENGTH_SHORT).show() 87 | if (mOldItems.size > 0) { 88 | mOldItems.removeAt(mOldItems.size - 1) 89 | mAdapter!!.notifyDataSetChanged() 90 | } 91 | }) 92 | } 93 | 94 | fun onSetAdapter(list: List?) { 95 | val newItems = Items(list!!) 96 | newItems.add(FooterBean()) 97 | 98 | DiffCallback.create(mOldItems, newItems, mAdapter!!) 99 | mOldItems.clear() 100 | mOldItems.addAll(newItems) 101 | 102 | mCanloadmore = true 103 | 104 | recycler_view.addOnScrollListener(object : OnLoadMoreListener() { 105 | override fun onLoadMore() { 106 | if (mCanloadmore) { 107 | mCanloadmore = false 108 | mModel.loadMore() 109 | } 110 | } 111 | }) 112 | } 113 | 114 | override fun onRefresh() { 115 | mModel.doRefresh() 116 | } 117 | 118 | fun onShowLoading() { 119 | refresh_layout.isRefreshing = true 120 | } 121 | 122 | fun onHideLoading() { 123 | refresh_layout.isRefreshing = false 124 | } 125 | 126 | fun onShowNetError() { 127 | Snackbar.make(refresh_layout, R.string.network_error, Snackbar.LENGTH_SHORT).show() 128 | } 129 | 130 | companion object { 131 | 132 | const val ARGUMENT_SLUG = "ARGUMENT_SLUG" 133 | const val ARGUMENT_NAME = "ARGUMENT_NAME" 134 | const val ARGUMENT_POSTSCOUNT = "ARGUMENT_POSTSCOUNT" 135 | const val TAG = "PostsListView" 136 | 137 | fun newInstance(slug: String, title: String, postsCount: Int): PostsListView { 138 | val args = Bundle() 139 | args.putString(ARGUMENT_SLUG, slug) 140 | args.putString(ARGUMENT_NAME, title) 141 | args.putInt(ARGUMENT_POSTSCOUNT, postsCount) 142 | val fragment = PostsListView() 143 | fragment.arguments = args 144 | return fragment 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/postslist/PostsListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.postslist 2 | 3 | import android.app.Application 4 | import android.arch.lifecycle.* 5 | import com.meiji.daily.bean.PostsListBean 6 | import com.meiji.daily.data.remote.IApi 7 | import com.meiji.daily.io 8 | import com.meiji.daily.mainThread 9 | import com.meiji.daily.util.ErrorAction 10 | import io.reactivex.disposables.CompositeDisposable 11 | import io.reactivex.functions.Consumer 12 | import retrofit2.Retrofit 13 | import java.util.* 14 | 15 | /** 16 | * Created by Meiji on 2017/12/5. 17 | */ 18 | 19 | class PostsListViewModel 20 | constructor(application: Application, 21 | private val mSlug: String, 22 | private val mPostCount: Int, 23 | private val mRetrofit: Retrofit) : AndroidViewModel(application) { 24 | private val mDisposable: CompositeDisposable 25 | private var mList: MutableList 26 | var isLoading: MutableLiveData 27 | private set 28 | var isEnd: MutableLiveData 29 | private set 30 | var mOffset: MutableLiveData 31 | var mListLiveData: LiveData> 32 | private set 33 | 34 | init { 35 | mList = ArrayList() 36 | mDisposable = CompositeDisposable() 37 | isLoading = MutableLiveData() 38 | isEnd = MutableLiveData() 39 | mListLiveData = MutableLiveData() 40 | mOffset = MutableLiveData() 41 | 42 | isLoading.value = true 43 | mOffset.value = 0 44 | 45 | // 当 mOffset 的值发生改变,就会执行 apply 46 | mListLiveData = Transformations.switchMap(mOffset) { offset -> handleData(offset!!) } 47 | } 48 | 49 | private fun handleData(offset: Int): LiveData> { 50 | 51 | val liveData = MutableLiveData>() 52 | 53 | mRetrofit.create(IApi::class.java).getPostsList(mSlug, offset) 54 | .subscribeOn(io) 55 | .observeOn(mainThread) 56 | .subscribe(Consumer { list -> 57 | mList.addAll(list) 58 | liveData.value = mList 59 | isLoading.value = false 60 | }, object : ErrorAction() { 61 | override fun doAction() { 62 | liveData.value = null 63 | } 64 | }.action()).let { mDisposable.add(it) } 65 | return liveData 66 | } 67 | 68 | internal fun doRefresh() { 69 | mList.clear() 70 | mOffset.value = 0 71 | } 72 | 73 | internal fun loadMore() { 74 | if (mOffset.value != null && mOffset.value!! >= mPostCount - 1) { 75 | isEnd.value = true 76 | return 77 | } 78 | 79 | mOffset.value = mList.size 80 | } 81 | 82 | override fun onCleared() { 83 | mDisposable.clear() 84 | super.onCleared() 85 | } 86 | 87 | class Factory internal constructor(private val mApplication: Application, 88 | private val mSlug: String, 89 | private val mPostCount: Int, 90 | private val mRetrofit: Retrofit) : ViewModelProvider.NewInstanceFactory() { 91 | 92 | override fun create(modelClass: Class): T { 93 | return com.meiji.daily.module.postslist.PostsListViewModel(mApplication, mSlug, mPostCount, mRetrofit) as T 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/shareadd/ShareAddActivity.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.shareadd 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.os.Handler 6 | import android.support.v7.app.AppCompatActivity 7 | import android.text.TextUtils 8 | import android.widget.Toast 9 | import com.afollestad.materialdialogs.MaterialDialog 10 | import com.afollestad.materialdialogs.Theme 11 | import com.meiji.daily.* 12 | import com.meiji.daily.data.local.AppDatabase 13 | import com.meiji.daily.data.remote.IApi 14 | import com.meiji.daily.util.SettingHelper 15 | import io.reactivex.disposables.CompositeDisposable 16 | import io.reactivex.functions.Consumer 17 | import retrofit2.Retrofit 18 | import java.util.regex.Pattern 19 | import javax.inject.Inject 20 | 21 | /** 22 | * Created by Meiji on 2016/12/1. 23 | */ 24 | 25 | class ShareAddActivity : AppCompatActivity() { 26 | private val mDisposable: CompositeDisposable = CompositeDisposable() 27 | @Inject 28 | lateinit var mSettingHelper: SettingHelper 29 | @Inject 30 | lateinit var mAppDatabase: AppDatabase 31 | @Inject 32 | lateinit var mRetrofit: Retrofit 33 | private lateinit var mDialog: MaterialDialog 34 | 35 | override fun onCreate(savedInstanceState: Bundle?) { 36 | DaggerShareAddComponent.builder() 37 | .appComponent(App.sAppComponent) 38 | .build().inject(this) 39 | super.onCreate(savedInstanceState) 40 | mDialog = MaterialDialog.Builder(this) 41 | .progress(true, 0) 42 | .content(R.string.md_loading) 43 | .theme(if (mSettingHelper.isNightMode) Theme.DARK else Theme.LIGHT) 44 | .cancelable(true) 45 | .build() 46 | mDialog.show() 47 | 48 | val intent = intent 49 | val action = intent.action 50 | val type = intent.type 51 | val shareText = intent.getStringExtra(Intent.EXTRA_TEXT) 52 | if (action == Intent.ACTION_SEND && type == "text/plain" && !TextUtils.isEmpty(shareText)) { 53 | handleSendText(shareText) 54 | } else { 55 | onFinish(getString(R.string.formal_incorrect)) 56 | } 57 | } 58 | 59 | private fun handleSendText(shareText: String) { 60 | 61 | val regex = "^.*http.*://zhuanlan.zhihu.com/(.*)$" 62 | val matcher = Pattern.compile(regex).matcher(shareText) 63 | if (matcher.find()) { 64 | val slug = matcher.group(1).toLowerCase() 65 | mAppDatabase.zhuanlanDao().query(Constant.TYPE_USERADD) 66 | .subscribeOn(io) 67 | .observeOn(mainThread) 68 | .subscribe(Consumer { list -> 69 | for (bean in list) { 70 | if (bean.slug == slug) { 71 | onFinish(getString(R.string.has_been_added)) 72 | return@Consumer 73 | } 74 | } 75 | }).let { mDisposable.add(it) } 76 | 77 | mRetrofit.create(IApi::class.java).getZhuanlanBean(slug) 78 | .map { bean -> 79 | bean.type = Constant.TYPE_USERADD 80 | return@map mAppDatabase.zhuanlanDao().insert(bean).toInt() != -1 81 | } 82 | .subscribeOn(io) 83 | .observeOn(mainThread) 84 | .subscribe({ isSuccess -> 85 | if (isSuccess!!) { 86 | onFinish(getString(R.string.add_zhuanlan_id_success)) 87 | } else { 88 | onFinish(getString(R.string.add_zhuanlan_id_error)) 89 | } 90 | }) { onFinish(getString(R.string.add_zhuanlan_id_error)) } 91 | .let { mDisposable.add(it) } 92 | } else { 93 | onFinish(getString(R.string.incorrect_link)) 94 | } 95 | } 96 | 97 | override fun onDestroy() { 98 | mDisposable.clear() 99 | super.onDestroy() 100 | } 101 | 102 | private fun onFinish(message: String) { 103 | mDialog.dismiss() 104 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 105 | Handler().postDelayed({ finish() }, 800) 106 | } 107 | 108 | companion object { 109 | 110 | internal val TAG = "ShareAddActivity" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/shareadd/ShareAddComponent.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.shareadd 2 | 3 | import com.meiji.daily.di.component.AppComponent 4 | import com.meiji.daily.di.scope.ActivityScoped 5 | 6 | import dagger.Component 7 | 8 | /** 9 | * Created by Meiji on 2017/12/28. 10 | */ 11 | @ActivityScoped 12 | @Component(dependencies = arrayOf(AppComponent::class)) 13 | interface ShareAddComponent { 14 | 15 | fun inject(shareAddActivity: ShareAddActivity) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/useradd/UserAddComponent.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.useradd 2 | 3 | import com.meiji.daily.di.component.AppComponent 4 | import com.meiji.daily.di.scope.FragmentScoped 5 | 6 | import dagger.Component 7 | 8 | /** 9 | * Created by Meiji on 2017/12/28. 10 | */ 11 | @FragmentScoped 12 | @Component(modules = arrayOf(UserAddModule::class), dependencies = arrayOf(AppComponent::class)) 13 | interface UserAddComponent { 14 | 15 | fun inject(userAddView: UserAddView) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/useradd/UserAddModule.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.useradd 2 | 3 | import android.app.Application 4 | import android.arch.lifecycle.ViewModelProviders 5 | import com.meiji.daily.data.local.AppDatabase 6 | import com.meiji.daily.di.scope.FragmentScoped 7 | import com.meiji.daily.util.RxBusHelper 8 | import dagger.Module 9 | import dagger.Provides 10 | import retrofit2.Retrofit 11 | import javax.inject.Named 12 | 13 | /** 14 | * Created by Meiji on 2017/12/28. 15 | */ 16 | @Module 17 | class UserAddModule(private val mUserAddView: UserAddView) { 18 | 19 | @FragmentScoped 20 | @Provides 21 | internal fun provideModel(@Named("application") application: Application, 22 | appDatabase: AppDatabase, 23 | retrofit: Retrofit, 24 | rxBusHelper: RxBusHelper): UserAddViewModel { 25 | val factory = UserAddViewModel.Factory(application, appDatabase, retrofit, rxBusHelper) 26 | return ViewModelProviders.of(mUserAddView, factory).get(UserAddViewModel::class.java) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/useradd/UserAddViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.useradd 2 | 3 | import android.app.Application 4 | import android.arch.lifecycle.AndroidViewModel 5 | import android.arch.lifecycle.MutableLiveData 6 | import android.arch.lifecycle.ViewModel 7 | import android.arch.lifecycle.ViewModelProvider 8 | import com.meiji.daily.Constant 9 | import com.meiji.daily.bean.ZhuanlanBean 10 | import com.meiji.daily.data.local.AppDatabase 11 | import com.meiji.daily.data.remote.IApi 12 | import com.meiji.daily.io 13 | import com.meiji.daily.mainThread 14 | import com.meiji.daily.util.ErrorAction 15 | import com.meiji.daily.util.RxBusHelper 16 | import io.reactivex.Flowable 17 | import io.reactivex.Single 18 | import io.reactivex.SingleOnSubscribe 19 | import io.reactivex.disposables.CompositeDisposable 20 | import io.reactivex.functions.Consumer 21 | import retrofit2.Retrofit 22 | 23 | /** 24 | * Created by Meiji on 2017/12/4. 25 | */ 26 | 27 | class UserAddViewModel 28 | constructor(application: Application, 29 | private val mAppDatabase: AppDatabase, 30 | private val mRetrofit: Retrofit, 31 | private val mRxBusHelper: RxBusHelper) 32 | : AndroidViewModel(application) { 33 | 34 | private val mDisposable: CompositeDisposable 35 | 36 | private lateinit var mRxBus: Flowable 37 | var isLoading: MutableLiveData 38 | private set 39 | var isRefreshUI: MutableLiveData 40 | private set 41 | var isAddResult: MutableLiveData 42 | private set 43 | var mList: MutableLiveData> 44 | private set 45 | 46 | init { 47 | isLoading = MutableLiveData() 48 | isRefreshUI = MutableLiveData() 49 | isAddResult = MutableLiveData() 50 | mList = MutableLiveData() 51 | mDisposable = CompositeDisposable() 52 | 53 | isLoading.value = true 54 | isRefreshUI.value = true 55 | } 56 | 57 | init { 58 | handleData() 59 | subscribeTheme() 60 | } 61 | 62 | fun handleData() { 63 | isLoading.value = true 64 | 65 | mAppDatabase.zhuanlanDao().query(Constant.TYPE_USERADD) 66 | .subscribeOn(io) 67 | .observeOn(mainThread) 68 | .subscribe(Consumer> { list -> 69 | mList.value = list 70 | }, ErrorAction.error()).let { mDisposable.add(it) } 71 | isLoading.value = false 72 | } 73 | 74 | internal fun addItem(input: String) { 75 | isLoading.value = true 76 | 77 | mRetrofit.create(IApi::class.java).getZhuanlanBean(input) 78 | .doOnSuccess { bean -> 79 | bean?.let { 80 | it.type = Constant.TYPE_USERADD 81 | mAppDatabase.zhuanlanDao().insert(it) 82 | } 83 | } 84 | .subscribeOn(io) 85 | .observeOn(mainThread) 86 | .subscribe(Consumer { 87 | isAddResult.value = true 88 | handleData() 89 | }, object : ErrorAction() { 90 | override fun doAction() { 91 | isAddResult.value = false 92 | isLoading.value = false 93 | } 94 | }.action()).let { mDisposable.add(it) } 95 | } 96 | 97 | private fun subscribeTheme() { 98 | mRxBus = mRxBusHelper.register(Constant.RxBusEvent.REFRESHUI) 99 | mRxBus.subscribe(Consumer { 100 | isRefreshUI.setValue(!(isRefreshUI.value)!!) 101 | }, ErrorAction.error()).let { mDisposable.add(it) } 102 | } 103 | 104 | internal fun deleteItem(bean: ZhuanlanBean) { 105 | Single.create(SingleOnSubscribe { 106 | mAppDatabase.zhuanlanDao().delete(bean.slug) 107 | }).subscribeOn(io) 108 | .subscribe().let { mDisposable.add(it) } 109 | 110 | } 111 | 112 | override fun onCleared() { 113 | mRxBusHelper.unregister(Constant.RxBusEvent.REFRESHUI, mRxBus) 114 | mDisposable.clear() 115 | super.onCleared() 116 | } 117 | 118 | class Factory internal constructor(private val mApplication: Application, 119 | private val mAppDatabase: AppDatabase, 120 | private val mRetrofit: Retrofit, 121 | private val mRxBusHelper: RxBusHelper) 122 | : ViewModelProvider.NewInstanceFactory() { 123 | 124 | override fun create(modelClass: Class): T { 125 | return UserAddViewModel(mApplication, mAppDatabase, mRetrofit, mRxBusHelper) as T 126 | } 127 | } 128 | } 129 | 130 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/zhuanlan/ZhuanlanComponent.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.zhuanlan 2 | 3 | import com.meiji.daily.di.component.AppComponent 4 | import com.meiji.daily.di.scope.FragmentScoped 5 | 6 | import dagger.Component 7 | 8 | /** 9 | * Created by Meiji on 2017/12/21. 10 | */ 11 | @FragmentScoped 12 | @Component(modules = arrayOf(ZhuanlanModule::class), dependencies = arrayOf(AppComponent::class)) 13 | interface ZhuanlanComponent { 14 | 15 | // 与 dependencies 有冲突 16 | // @Component.Builder 17 | // interface Builder { 18 | // Builder injectView(ZhuanlanView view); 19 | // 20 | // Builder injectType(int type); 21 | // 22 | // ZhuanlanComponent build(); 23 | // } 24 | 25 | fun inject(view: ZhuanlanView) 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/zhuanlan/ZhuanlanModule.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.zhuanlan 2 | 3 | import android.app.Application 4 | import android.arch.lifecycle.ViewModelProviders 5 | import com.meiji.daily.Constant 6 | import com.meiji.daily.data.local.AppDatabase 7 | import com.meiji.daily.di.scope.FragmentScoped 8 | import com.meiji.daily.util.RxBusHelper 9 | import dagger.Module 10 | import dagger.Provides 11 | import retrofit2.Retrofit 12 | import javax.inject.Named 13 | 14 | /** 15 | * Created by Meiji on 2017/12/21. 16 | */ 17 | @Module 18 | class ZhuanlanModule(private val mZhuanlanView: ZhuanlanView) { 19 | 20 | @FragmentScoped 21 | @Provides 22 | internal fun provideModel(@Named("application") application: Application, 23 | @Named("type") type: Int, 24 | appDatabase: AppDatabase, 25 | retrofit: Retrofit, 26 | rxBusHelper: RxBusHelper): ZhuanlanViewModel { 27 | val factory = ZhuanlanViewModel.Factory(application, type, appDatabase, retrofit, rxBusHelper) 28 | return ViewModelProviders.of(mZhuanlanView, factory).get(ZhuanlanViewModel::class.java) 29 | } 30 | 31 | @FragmentScoped 32 | @Provides 33 | @Named("type") 34 | internal fun provideType(): Int { 35 | val bundle = mZhuanlanView.arguments 36 | var type = Constant.TYPE_PRODUCT 37 | bundle?.run { 38 | type = bundle.getInt(ZhuanlanView.ARGUMENT_TYPE, Constant.TYPE_PRODUCT) 39 | } 40 | return type 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/zhuanlan/ZhuanlanView.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.zhuanlan 2 | 3 | import android.arch.lifecycle.Observer 4 | import android.content.SharedPreferences 5 | import android.os.Bundle 6 | import android.support.design.widget.Snackbar 7 | import android.support.v4.widget.SwipeRefreshLayout 8 | import android.support.v7.widget.LinearLayoutManager 9 | import android.util.TypedValue 10 | import android.view.View 11 | import com.meiji.daily.App 12 | import com.meiji.daily.R 13 | import com.meiji.daily.bean.ZhuanlanBean 14 | import com.meiji.daily.binder.ZhuanlanViewBinder 15 | import com.meiji.daily.module.base.BaseFragment 16 | import com.meiji.daily.util.RecyclerViewUtil 17 | import com.meiji.daily.util.SettingHelper 18 | import kotlinx.android.synthetic.main.fragment_zhuanlan.* 19 | import kotlinx.android.synthetic.main.fragment_zhuanlan.view.* 20 | import kotlinx.android.synthetic.main.item_zhuanlan.view.* 21 | import me.drakeet.multitype.MultiTypeAdapter 22 | import javax.inject.Inject 23 | 24 | 25 | /** 26 | * Created by Meiji on 2017/11/29. 27 | */ 28 | 29 | class ZhuanlanView : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, SharedPreferences.OnSharedPreferenceChangeListener { 30 | 31 | @Inject 32 | lateinit var mModel: ZhuanlanViewModel 33 | @Inject 34 | lateinit var mSettingHelper: SettingHelper 35 | @Inject 36 | lateinit var mSharedPreferences: SharedPreferences 37 | 38 | private var mAdapter: MultiTypeAdapter? = null 39 | @Suppress("deprecation") 40 | private fun refreshUI() { 41 | val theme = activity!!.theme 42 | val rootViewBackground = TypedValue() 43 | val itemViewBackground = TypedValue() 44 | val textColorPrimary = TypedValue() 45 | theme.resolveAttribute(R.attr.rootViewBackground, rootViewBackground, true) 46 | theme.resolveAttribute(R.attr.itemViewBackground, itemViewBackground, true) 47 | theme.resolveAttribute(R.attr.textColorPrimary, textColorPrimary, true) 48 | ll_root!!.setBackgroundResource(rootViewBackground.resourceId) 49 | 50 | val resources = resources 51 | val childCount = recycler_view!!.childCount 52 | for (i in 0 until childCount) { 53 | val cardView = recycler_view!!.getChildAt(i).cardview 54 | cardView.setBackgroundResource(itemViewBackground.resourceId) 55 | 56 | cardView.tv_name.setTextColor(resources.getColor(textColorPrimary.resourceId)) 57 | cardView.tv_followersCount.setTextColor(resources.getColor(textColorPrimary.resourceId)) 58 | cardView.tv_postsCount.setTextColor(resources.getColor(textColorPrimary.resourceId)) 59 | cardView.tv_intro.setTextColor(resources.getColor(textColorPrimary.resourceId)) 60 | } 61 | RecyclerViewUtil.invalidateCacheItem(recycler_view) 62 | } 63 | 64 | override fun initInject() { 65 | DaggerZhuanlanComponent.builder() 66 | .appComponent(App.sAppComponent) 67 | .zhuanlanModule(ZhuanlanModule(this)) 68 | .build().inject(this) 69 | } 70 | 71 | override fun attachLayoutId() = R.layout.fragment_zhuanlan 72 | 73 | override fun initViews(view: View) { 74 | view.recycler_view.setHasFixedSize(true) 75 | view.recycler_view.layoutManager = LinearLayoutManager(activity) 76 | view.refresh_layout.setColorSchemeColors(mSettingHelper.color) 77 | view.refresh_layout.setOnRefreshListener(this) 78 | } 79 | 80 | override fun onStop() { 81 | super.onStop() 82 | mSharedPreferences.unregisterOnSharedPreferenceChangeListener(this) 83 | } 84 | 85 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { 86 | refresh_layout.setColorSchemeColors(mSettingHelper.color) 87 | } 88 | 89 | override fun subscribeUI() { 90 | mModel.mList.observe(this, Observer> { list -> 91 | if (null != list && list.size > 0) { 92 | onSetAdapter(list) 93 | } else { 94 | onShowNetError() 95 | } 96 | }) 97 | mModel.isLoading.observe(this, Observer { aBoolean -> 98 | if (aBoolean!!) { 99 | onShowLoading() 100 | } else { 101 | onHideLoading() 102 | } 103 | }) 104 | mModel.isRefreshUI.observe(this, Observer { refreshUI() }) 105 | mSharedPreferences.registerOnSharedPreferenceChangeListener(this) 106 | } 107 | 108 | private fun onSetAdapter(list: List) { 109 | if (mAdapter == null) { 110 | mAdapter = MultiTypeAdapter(list) 111 | mAdapter?.register(ZhuanlanBean::class.java, ZhuanlanViewBinder()) 112 | recycler_view.adapter = mAdapter 113 | } else { 114 | mAdapter?.notifyDataSetChanged() 115 | } 116 | } 117 | 118 | override fun onRefresh() { 119 | mModel.handleData() 120 | } 121 | 122 | private fun onShowLoading() { 123 | refresh_layout.isRefreshing = true 124 | recycler_view.visibility = View.GONE 125 | } 126 | 127 | private fun onHideLoading() { 128 | refresh_layout.isRefreshing = false 129 | recycler_view.visibility = View.VISIBLE 130 | } 131 | 132 | private fun onShowNetError() { 133 | Snackbar.make(refresh_layout, R.string.network_error, Snackbar.LENGTH_SHORT).show() 134 | refresh_layout.isEnabled = true 135 | } 136 | 137 | companion object { 138 | 139 | internal val ARGUMENT_TYPE = "ARGUMENT_TYPE" 140 | internal val TAG = "ZhuanlanView" 141 | 142 | fun newInstance(type: Int): ZhuanlanView { 143 | val args = Bundle() 144 | args.putInt(ARGUMENT_TYPE, type) 145 | val fragment = ZhuanlanView() 146 | fragment.arguments = args 147 | return fragment 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/module/zhuanlan/ZhuanlanViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.module.zhuanlan 2 | 3 | import android.app.Application 4 | import android.arch.lifecycle.AndroidViewModel 5 | import android.arch.lifecycle.MutableLiveData 6 | import android.arch.lifecycle.ViewModel 7 | import android.arch.lifecycle.ViewModelProvider 8 | import com.meiji.daily.Constant 9 | import com.meiji.daily.R 10 | import com.meiji.daily.bean.ZhuanlanBean 11 | import com.meiji.daily.data.local.AppDatabase 12 | import com.meiji.daily.data.remote.IApi 13 | import com.meiji.daily.io 14 | import com.meiji.daily.mainThread 15 | import com.meiji.daily.util.ErrorAction 16 | import com.meiji.daily.util.RxBusHelper 17 | import io.reactivex.Flowable 18 | import io.reactivex.Maybe 19 | import io.reactivex.disposables.CompositeDisposable 20 | import io.reactivex.functions.Consumer 21 | import retrofit2.Retrofit 22 | import java.util.* 23 | 24 | /** 25 | * Created by Meiji on 2017/11/29. 26 | */ 27 | 28 | class ZhuanlanViewModel 29 | constructor(application: Application, 30 | private val mType: Int, 31 | private val mAppDatabase: AppDatabase, 32 | private val mRetrofit: Retrofit, 33 | private val mRxBusHelper: RxBusHelper) : AndroidViewModel(application) { 34 | 35 | private val mDisposable: CompositeDisposable 36 | private lateinit var mIdArr: Array 37 | private lateinit var mRxBus: Flowable 38 | var isLoading: MutableLiveData 39 | private set 40 | var isRefreshUI: MutableLiveData 41 | private set 42 | var mList: MutableLiveData> 43 | private set 44 | 45 | init { 46 | isLoading = MutableLiveData() 47 | isRefreshUI = MutableLiveData() 48 | mList = MutableLiveData() 49 | mDisposable = CompositeDisposable() 50 | 51 | isLoading.value = true 52 | isRefreshUI.value = true 53 | } 54 | 55 | init { 56 | handleData() 57 | subscribeTheme() 58 | } 59 | 60 | private fun subscribeTheme() { 61 | mRxBus = mRxBusHelper.register(Constant.RxBusEvent.REFRESHUI) 62 | mRxBus.subscribe(Consumer { 63 | isRefreshUI.setValue(!(isRefreshUI.value)!!) 64 | }, ErrorAction.error()).let { mDisposable.add(it) } 65 | } 66 | 67 | internal fun handleData() { 68 | mAppDatabase.zhuanlanDao().query(mType) 69 | .flatMap { 70 | if (it.size > 0) { 71 | return@flatMap Maybe.just(it) 72 | } else { 73 | val l = retrofitRequest() 74 | return@flatMap Maybe.just(l) 75 | } 76 | } 77 | .subscribeOn(io) 78 | .observeOn(mainThread) 79 | .subscribe(Consumer> { list -> 80 | mList.value = list 81 | isLoading.setValue(false) 82 | }, object : ErrorAction() { 83 | override fun doAction() { 84 | mList.value = null 85 | isLoading.value = false 86 | } 87 | }.action()).let { mDisposable.add(it) } 88 | } 89 | 90 | override fun onCleared() { 91 | mRxBusHelper.unregister(Constant.RxBusEvent.REFRESHUI, mRxBus) 92 | mDisposable.clear() 93 | super.onCleared() 94 | } 95 | 96 | private fun retrofitRequest(): List { 97 | 98 | val resources = getApplication().resources 99 | 100 | when (mType) { 101 | Constant.TYPE_PRODUCT -> mIdArr = resources.getStringArray(R.array.product) 102 | Constant.TYPE_MUSIC -> mIdArr = resources.getStringArray(R.array.music) 103 | Constant.TYPE_LIFE -> mIdArr = resources.getStringArray(R.array.life) 104 | Constant.TYPE_EMOTION -> mIdArr = resources.getStringArray(R.array.emotion) 105 | Constant.TYPE_FINANCE -> mIdArr = resources.getStringArray(R.array.profession) 106 | Constant.TYPE_ZHIHU -> mIdArr = resources.getStringArray(R.array.zhihu) 107 | } 108 | 109 | val list = ArrayList() 110 | val api = mRetrofit.create(IApi::class.java) 111 | 112 | val maybeList = mIdArr.map { api.getZhuanlanBean(it) } 113 | 114 | Maybe.merge(maybeList) 115 | .doOnComplete { mAppDatabase.zhuanlanDao().insert(list) } 116 | .subscribe(Consumer { bean -> 117 | if (bean != null) { 118 | bean.type = mType 119 | list.add(bean) 120 | } 121 | }, ErrorAction.error()).let { mDisposable.add(it) } 122 | 123 | return list 124 | } 125 | 126 | class Factory(private val mApplication: Application, 127 | private val mType: Int, 128 | private val mAppDatabase: AppDatabase, 129 | private val mRetrofit: Retrofit, 130 | private val mRxBusHelper: RxBusHelper) : ViewModelProvider.NewInstanceFactory() { 131 | 132 | override fun create(modelClass: Class): T { 133 | return ZhuanlanViewModel(mApplication, mType, mAppDatabase, mRetrofit, mRxBusHelper) as T 134 | } 135 | } 136 | 137 | companion object { 138 | 139 | internal val TAG = "ZhuanlanViewModel" 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/util/DiffCallback.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.util 2 | 3 | import android.support.v7.util.DiffUtil 4 | 5 | import me.drakeet.multitype.Items 6 | import me.drakeet.multitype.MultiTypeAdapter 7 | 8 | /** 9 | * Created by Meiji on 2018/1/25. 10 | */ 11 | 12 | class DiffCallback private constructor(private val oldItems: Items?, private val newItems: Items?) : DiffUtil.Callback() { 13 | 14 | override fun getOldListSize(): Int { 15 | return oldItems?.size ?: 0 16 | } 17 | 18 | override fun getNewListSize(): Int { 19 | return newItems?.size ?: 0 20 | } 21 | 22 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 23 | return oldItems!![oldItemPosition] == newItems!![newItemPosition] 24 | } 25 | 26 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 27 | return oldItems!![oldItemPosition].hashCode() == newItems!![newItemPosition].hashCode() 28 | } 29 | 30 | companion object { 31 | 32 | fun create(oldItems: Items, newItems: Items, adapter: MultiTypeAdapter) { 33 | val diffCallback = DiffCallback(oldItems, newItems) 34 | val result = DiffUtil.calculateDiff(diffCallback, true) 35 | result.dispatchUpdatesTo(adapter) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/util/ErrorAction.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.util 2 | 3 | import com.crashlytics.android.Crashlytics 4 | import com.meiji.daily.BuildConfig 5 | 6 | import io.reactivex.annotations.NonNull 7 | import io.reactivex.functions.Consumer 8 | 9 | /** 10 | * Created by Meiji on 2018/1/25. 11 | */ 12 | 13 | abstract class ErrorAction { 14 | 15 | fun action(): Consumer { 16 | return Consumer { throwable -> 17 | print(throwable) 18 | doAction() 19 | } 20 | } 21 | 22 | abstract fun doAction() 23 | 24 | companion object { 25 | 26 | fun error(): Consumer { 27 | return Consumer { throwable -> print(throwable) } 28 | } 29 | 30 | fun print(@NonNull throwable: Throwable) { 31 | if (BuildConfig.DEBUG) { 32 | throwable.printStackTrace() 33 | } else { 34 | Crashlytics.logException(throwable) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/util/JsonUtil.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.util 2 | 3 | import org.json.JSONArray 4 | import org.json.JSONException 5 | import org.json.JSONObject 6 | 7 | /** 8 | * Created by Meiji on 2018/2/6. 9 | */ 10 | 11 | object JsonUtil { 12 | 13 | private const val JSON_INDENT = 2 14 | 15 | /** 16 | * @param jsonStr string param expect a json string 17 | * @return formatted json string if param is a json string,otherwise return the param 18 | */ 19 | @Throws(JSONException::class) 20 | fun convert(jsonStr: String): String { 21 | val json = jsonStr.trim() 22 | return when { 23 | json.startsWith("{") && json.endsWith("}") -> { 24 | JSONObject(json).toString(JSON_INDENT) 25 | } 26 | 27 | json.startsWith("[") && json.endsWith("]") -> { 28 | JSONArray(json).toString(JSON_INDENT) 29 | } 30 | 31 | else -> json 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/util/NetWorkUtil.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.util 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | 6 | /** 7 | * Created by Meiji on 2017/5/2. 8 | */ 9 | 10 | object NetWorkUtil { 11 | 12 | /** 13 | * 判断是否有网络连接 14 | */ 15 | fun isNetworkConnected(context: Context): Boolean { 16 | // 获取手机所有连接管理对象(包括对wi-fi,net等连接的管理) 17 | val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 18 | // 获取NetworkInfo对象 19 | val networkInfo = manager.activeNetworkInfo 20 | //判断NetworkInfo对象是否为空 21 | return networkInfo?.isAvailable ?: false 22 | } 23 | 24 | /** 25 | * 判断WIFI网络是否可用 26 | */ 27 | fun isWifiConnected(context: Context): Boolean { 28 | // 获取手机所有连接管理对象(包括对wi-fi,net等连接的管理) 29 | val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 30 | // 获取NetworkInfo对象 31 | val networkInfo = manager.activeNetworkInfo 32 | //判断NetworkInfo对象是否为空 并且类型是否为WIFI 33 | return networkInfo?.isAvailable ?: false && networkInfo?.type == ConnectivityManager.TYPE_WIFI 34 | } 35 | 36 | /** 37 | * 判断MOBILE网络是否可用 38 | */ 39 | fun isMobileConnected(context: Context): Boolean { 40 | //获取手机所有连接管理对象(包括对wi-fi,net等连接的管理) 41 | val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 42 | //获取NetworkInfo对象 43 | val networkInfo = manager.activeNetworkInfo 44 | //判断NetworkInfo对象是否为空 并且类型是否为MOBILE 45 | return networkInfo?.isAvailable ?: false && networkInfo?.type == ConnectivityManager.TYPE_MOBILE 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/util/OnLoadMoreListener.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.util 2 | 3 | import android.support.v7.widget.LinearLayoutManager 4 | import android.support.v7.widget.RecyclerView 5 | import android.util.Log 6 | 7 | /** 8 | * Created by Meiji on 2017/6/8. 9 | */ 10 | 11 | abstract class OnLoadMoreListener : RecyclerView.OnScrollListener() { 12 | 13 | private var layoutManager: LinearLayoutManager? = null 14 | private var itemCount: Int = 0 15 | private var lastPosition: Int = 0 16 | private var lastItemCount: Int = 0 17 | 18 | abstract fun onLoadMore() 19 | 20 | override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) { 21 | if (recyclerView!!.layoutManager is LinearLayoutManager) { 22 | layoutManager = recyclerView.layoutManager as LinearLayoutManager 23 | 24 | itemCount = layoutManager!!.itemCount 25 | lastPosition = layoutManager!!.findLastCompletelyVisibleItemPosition() 26 | } else { 27 | Log.e("OnLoadMoreListener", "The OnLoadMoreListener only support LinearLayoutManager") 28 | return 29 | } 30 | 31 | if (lastItemCount != itemCount && lastPosition == itemCount - 1) { 32 | lastItemCount = itemCount 33 | this.onLoadMore() 34 | } 35 | } 36 | 37 | // @Override 38 | // public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 39 | // super.onScrollStateChanged(recyclerView, newState); 40 | // if (recyclerView.getLayoutManager() instanceof LinearLayoutManager) { 41 | // if (newState == RecyclerView.SCROLL_STATE_IDLE) { 42 | // if (!recyclerView.canScrollVertically(1)) { 43 | // this.doAction(); 44 | // } 45 | // } 46 | // } 47 | // } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/util/RecyclerViewUtil.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.util 2 | 3 | import android.support.v7.widget.RecyclerView 4 | 5 | /** 6 | * Created by Meiji on 2017/7/13. 7 | */ 8 | 9 | object RecyclerViewUtil { 10 | 11 | /** 12 | * 使RecyclerView缓存中在pool中的Item失效 13 | */ 14 | fun invalidateCacheItem(recyclerView: RecyclerView) { 15 | val recyclerViewClass = RecyclerView::class.java 16 | try { 17 | val declaredField = recyclerViewClass.getDeclaredField("mRecycler") 18 | declaredField.isAccessible = true 19 | val declaredMethod = Class.forName(RecyclerView.Recycler::class.java.name) 20 | .getDeclaredMethod("clear", *arrayOfNulls>(0) as Array>) 21 | declaredMethod.isAccessible = true 22 | declaredMethod.invoke(declaredField.get(recyclerView)) 23 | val recycledViewPool = recyclerView.recycledViewPool 24 | recycledViewPool.clear() 25 | } catch (e: Exception) { 26 | e.printStackTrace() 27 | } 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/util/RetrofitFactory.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.util 2 | 3 | import com.franmontiel.persistentcookiejar.PersistentCookieJar 4 | import com.franmontiel.persistentcookiejar.cache.SetCookieCache 5 | import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor 6 | import com.meiji.daily.App 7 | import com.meiji.daily.BuildConfig 8 | import com.meiji.daily.SdkManager 9 | import com.meiji.daily.data.remote.IApi 10 | import okhttp3.Cache 11 | import okhttp3.CacheControl 12 | import okhttp3.Interceptor 13 | import okhttp3.OkHttpClient 14 | import retrofit2.Retrofit 15 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 16 | import retrofit2.converter.moshi.MoshiConverterFactory 17 | import java.io.File 18 | import java.util.concurrent.TimeUnit 19 | 20 | /** 21 | * Created by Meiji on 2017/4/22. 22 | */ 23 | @Suppress("deprecation") 24 | @Deprecated("") 25 | class RetrofitFactory private constructor() { 26 | 27 | companion object { 28 | val instance: Retrofit by lazy { RetrofitFactory().init() } 29 | } 30 | 31 | /** 32 | * 缓存机制 33 | * 在响应请求之后在 data/data/<包名>/cache 下建立一个response 文件夹,保持缓存数据。 34 | * 这样我们就可以在请求的时候,如果判断到没有网络,自动读取缓存的数据。 35 | * 同样这也可以实现,在我们没有网络的情况下,重新打开App可以浏览的之前显示过的内容。 36 | * 也就是:判断网络,有网络,则从网络获取,并保存到缓存中,无网络,则从缓存中获取。 37 | * https://werb.github.io/2016/07/29/%E4%BD%BF%E7%94%A8Retrofit2+OkHttp3%E5%AE%9E%E7%8E%B0%E7%BC%93%E5%AD%98%E5%A4%84%E7%90%86/ 38 | */ 39 | private val cacheControlInterceptor = Interceptor { chain -> 40 | var request = chain.request() 41 | if (!NetWorkUtil.isNetworkConnected(App.sAppContext)) { 42 | request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build() 43 | } 44 | 45 | val originalResponse = chain.proceed(request) 46 | if (NetWorkUtil.isNetworkConnected(App.sAppContext)) { 47 | // 有网络时 设置缓存为默认值 48 | val cacheControl = request.cacheControl().toString() 49 | originalResponse.newBuilder() 50 | .header("Cache-Control", cacheControl) 51 | .removeHeader("Pragma") // 清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效 52 | .build() 53 | } else { 54 | // 无网络时 设置超时为1周 55 | val maxStale = 60 * 60 * 24 * 7 56 | originalResponse.newBuilder() 57 | .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale) 58 | .removeHeader("Pragma") 59 | .build() 60 | } 61 | } 62 | 63 | fun init(): Retrofit { 64 | // 指定缓存路径,缓存大小 50Mb 65 | val cache = Cache(File(App.sAppContext.cacheDir, "HttpCache"), 66 | (1024 * 1024 * 50).toLong()) 67 | // Cookie 持久化 68 | val cookieJar = PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(App.sAppContext)) 69 | 70 | var builder: OkHttpClient.Builder = OkHttpClient.Builder() 71 | .cookieJar(cookieJar) 72 | .cache(cache) 73 | .addInterceptor(cacheControlInterceptor) 74 | .connectTimeout(10, TimeUnit.SECONDS) 75 | .readTimeout(15, TimeUnit.SECONDS) 76 | .writeTimeout(15, TimeUnit.SECONDS) 77 | .retryOnConnectionFailure(true) 78 | 79 | // Log 拦截器 80 | if (BuildConfig.DEBUG) { 81 | builder = SdkManager.initInterceptor(builder) 82 | } 83 | 84 | return Retrofit.Builder() 85 | .baseUrl(IApi.API_BASE) 86 | .client(builder.build()) 87 | .addConverterFactory(MoshiConverterFactory.create()) 88 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 89 | .build() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/util/RetrofitHelper.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.util 2 | 3 | import android.content.Context 4 | import com.franmontiel.persistentcookiejar.PersistentCookieJar 5 | import com.franmontiel.persistentcookiejar.cache.SetCookieCache 6 | import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor 7 | import com.meiji.daily.BuildConfig 8 | import com.meiji.daily.SdkManager 9 | import com.meiji.daily.data.remote.IApi 10 | import okhttp3.Cache 11 | import okhttp3.CacheControl 12 | import okhttp3.Interceptor 13 | import okhttp3.OkHttpClient 14 | import retrofit2.Retrofit 15 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 16 | import retrofit2.converter.moshi.MoshiConverterFactory 17 | import java.io.File 18 | import java.util.concurrent.TimeUnit 19 | import javax.inject.Inject 20 | import javax.inject.Singleton 21 | 22 | /** 23 | * Created by Meiji on 2017/12/28. 24 | */ 25 | @Singleton 26 | class RetrofitHelper 27 | @Inject 28 | constructor(private val mContext: Context) { 29 | /** 30 | * 缓存机制 31 | * 在响应请求之后在 data/data/<包名>/cache 下建立一个response 文件夹,保持缓存数据。 32 | * 这样我们就可以在请求的时候,如果判断到没有网络,自动读取缓存的数据。 33 | * 同样这也可以实现,在我们没有网络的情况下,重新打开App可以浏览的之前显示过的内容。 34 | * 也就是:判断网络,有网络,则从网络获取,并保存到缓存中,无网络,则从缓存中获取。 35 | * https://werb.github.io/2016/07/29/%E4%BD%BF%E7%94%A8Retrofit2+OkHttp3%E5%AE%9E%E7%8E%B0%E7%BC%93%E5%AD%98%E5%A4%84%E7%90%86/ 36 | */ 37 | private val cacheControlInterceptor = Interceptor { chain -> 38 | var request = chain.request() 39 | if (!NetWorkUtil.isNetworkConnected(mContext)) { 40 | request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build() 41 | } 42 | 43 | val originalResponse = chain.proceed(request) 44 | if (NetWorkUtil.isNetworkConnected(mContext)) { 45 | // 有网络时 设置缓存为默认值 46 | val cacheControl = request.cacheControl().toString() 47 | originalResponse.newBuilder() 48 | .header("Cache-Control", cacheControl) 49 | .removeHeader("Pragma") // 清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效 50 | .build() 51 | } else { 52 | // 无网络时 设置超时为1周 53 | val maxStale = 60 * 60 * 24 * 7 54 | originalResponse.newBuilder() 55 | .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale) 56 | .removeHeader("Pragma") 57 | .build() 58 | } 59 | } 60 | 61 | // 指定缓存路径,缓存大小 50Mb 62 | // Cookie 持久化 63 | // Log 拦截器 64 | val retrofit: Retrofit 65 | get() { 66 | val cache = Cache(File(mContext.cacheDir, "HttpCache"), 67 | (1024 * 1024 * 50).toLong()) 68 | val cookieJar = PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(mContext)) 69 | 70 | var builder: OkHttpClient.Builder = OkHttpClient.Builder() 71 | .cookieJar(cookieJar) 72 | .cache(cache) 73 | .addInterceptor(cacheControlInterceptor) 74 | .connectTimeout(10, TimeUnit.SECONDS) 75 | .readTimeout(15, TimeUnit.SECONDS) 76 | .writeTimeout(15, TimeUnit.SECONDS) 77 | .retryOnConnectionFailure(true) 78 | if (BuildConfig.DEBUG) { 79 | builder = SdkManager.initInterceptor(builder) 80 | } 81 | 82 | return Retrofit.Builder() 83 | .baseUrl(IApi.API_BASE) 84 | .client(builder.build()) 85 | .addConverterFactory(MoshiConverterFactory.create()) 86 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 87 | .build() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/util/RxBus.java: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.util; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | 9 | import io.reactivex.Flowable; 10 | import io.reactivex.processors.PublishProcessor; 11 | 12 | /** 13 | * Created by Meiji on 2017/12/14. 14 | * 参考 :https://juejin.im/entry/58ff2e26a0bb9f0065d2c5f2 15 | */ 16 | @SuppressWarnings("all") 17 | @Deprecated 18 | public class RxBus { 19 | 20 | private ConcurrentHashMap> mPublishMap = new ConcurrentHashMap<>(); 21 | 22 | private RxBus() { 23 | 24 | } 25 | 26 | public static RxBus getInstance() { 27 | return Holder.sInstance; 28 | } 29 | 30 | public Flowable register(@NonNull Class clz) { 31 | return register(clz.getName()); 32 | } 33 | 34 | public Flowable register(@NonNull Object tag) { 35 | List processorList = mPublishMap.get(tag); 36 | if (null == processorList) { 37 | processorList = new ArrayList<>(); 38 | mPublishMap.put(tag, processorList); 39 | } 40 | 41 | PublishProcessor processor = PublishProcessor.create(); 42 | processorList.add(processor); 43 | 44 | //System.out.println("注册到rxbus"); 45 | return processor; 46 | } 47 | 48 | public void unregister(@NonNull Class clz, @NonNull Flowable flowable) { 49 | unregister(clz.getName(), flowable); 50 | } 51 | 52 | public void unregister(@NonNull Object tag, @NonNull Flowable flowable) { 53 | List processorList = mPublishMap.get(tag); 54 | if (null != processorList) { 55 | processorList.remove(flowable); 56 | if (processorList.isEmpty()) { 57 | mPublishMap.remove(tag); 58 | //System.out.println("从rxbus取消注册"); 59 | } 60 | } 61 | } 62 | 63 | public void post(@NonNull Object content) { 64 | post(content.getClass().getName(), content); 65 | } 66 | 67 | public void post(@NonNull Object tag, @NonNull Object content) { 68 | List processorList = mPublishMap.get(tag); 69 | if (!processorList.isEmpty()) { 70 | for (PublishProcessor processor : processorList) { 71 | processor.onNext(content); 72 | } 73 | } 74 | } 75 | 76 | private static class Holder { 77 | private static RxBus sInstance = new RxBus(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/util/RxBusHelper.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.util 2 | 3 | import io.reactivex.Flowable 4 | import io.reactivex.processors.PublishProcessor 5 | import java.util.concurrent.ConcurrentHashMap 6 | import javax.inject.Inject 7 | import javax.inject.Singleton 8 | 9 | /** 10 | * Created by Meiji on 2017/12/29. 11 | * 参考 :https://juejin.im/entry/58ff2e26a0bb9f0065d2c5f2 12 | */ 13 | 14 | @Singleton 15 | class RxBusHelper @Inject constructor() { 16 | 17 | private val mPublishMap = ConcurrentHashMap>>() 18 | 19 | fun register(clz: Class): Flowable { 20 | return register(clz.name) 21 | } 22 | 23 | fun register(tag: Any): Flowable { 24 | var processorList = mPublishMap[tag] 25 | if (null == processorList) { 26 | processorList = mutableListOf() 27 | mPublishMap[tag] = processorList 28 | } 29 | 30 | val processor = PublishProcessor.create() 31 | processorList.add(processor) 32 | 33 | //System.out.println("注册到rxbus"); 34 | return processor 35 | } 36 | 37 | fun unregister(clz: Class, flowable: Flowable<*>) { 38 | unregister(clz.name, flowable) 39 | } 40 | 41 | fun unregister(tag: Any, flowable: Flowable<*>) { 42 | val processorList = mPublishMap[tag] 43 | if (null != processorList) { 44 | processorList.remove(flowable) 45 | if (processorList.isEmpty()) { 46 | mPublishMap.remove(tag) 47 | //System.out.println("从rxbus取消注册"); 48 | } 49 | } 50 | } 51 | 52 | fun post(content: Any) { 53 | post(content.javaClass.name, content) 54 | } 55 | 56 | fun post(tag: Any, content: Any) { 57 | val processorList = mPublishMap[tag] 58 | if (!processorList?.isEmpty()!!) { 59 | for (processor in processorList) { 60 | processor.onNext(content) 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/util/SettingHelper.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.util 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.graphics.Color 6 | 7 | import com.meiji.daily.R 8 | 9 | import javax.inject.Inject 10 | import javax.inject.Singleton 11 | 12 | /** 13 | * Created by Meiji on 2017/12/28. 14 | */ 15 | 16 | @Singleton 17 | class SettingHelper 18 | @Inject 19 | constructor(private val mPreferences: SharedPreferences, private val mContext: Context) { 20 | 21 | val isNoPhotoMode: Boolean 22 | get() = mPreferences.getBoolean("switch_noPhotoMode", false) && NetWorkUtil.isMobileConnected(mContext) 23 | 24 | @Suppress("deprecation") 25 | var color: Int 26 | get() { 27 | val defaultColor = mContext.resources.getColor(R.color.colorPrimary) 28 | val color = mPreferences.getInt("color", defaultColor) 29 | return if (color != 0 && Color.alpha(color) != 255) { 30 | defaultColor 31 | } else color 32 | } 33 | set(color) = mPreferences.edit().putInt("color", color).apply() 34 | 35 | var isNightMode: Boolean 36 | get() = mPreferences.getBoolean("switch_nightMode", false) 37 | set(flag) = mPreferences.edit().putBoolean("switch_nightMode", flag).apply() 38 | 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/meiji/daily/util/SettingUtil.java: -------------------------------------------------------------------------------- 1 | package com.meiji.daily.util; 2 | 3 | import android.content.SharedPreferences; 4 | import android.graphics.Color; 5 | import android.preference.PreferenceManager; 6 | 7 | import com.meiji.daily.App; 8 | import com.meiji.daily.R; 9 | 10 | /** 11 | * Created by Meiji on 2017/2/20. 12 | */ 13 | @SuppressWarnings("all") 14 | @Deprecated 15 | public class SettingUtil { 16 | 17 | private SharedPreferences setting = PreferenceManager.getDefaultSharedPreferences(App.Companion.getSAppContext()); 18 | 19 | public static SettingUtil getInstance() { 20 | return SettingsUtilInstance.instance; 21 | } 22 | 23 | public boolean getIsNoPhotoMode() { 24 | return setting.getBoolean("switch_noPhotoMode", false) && NetWorkUtil.INSTANCE.isMobileConnected(App.Companion.getSAppContext()); 25 | } 26 | 27 | public int getColor() { 28 | int defaultColor = App.Companion.getSAppContext().getResources().getColor(R.color.colorPrimary); 29 | int color = setting.getInt("color", defaultColor); 30 | if ((color != 0) && Color.alpha(color) != 255) { 31 | return defaultColor; 32 | } 33 | return color; 34 | } 35 | 36 | public void setColor(int color) { 37 | setting.edit().putInt("color", color).apply(); 38 | } 39 | 40 | public boolean getIsNightMode() { 41 | return setting.getBoolean("switch_nightMode", false); 42 | } 43 | 44 | public void setIsNightMode(boolean flag) { 45 | setting.edit().putBoolean("switch_nightMode", flag).apply(); 46 | } 47 | 48 | private static final class SettingsUtilInstance { 49 | private static final SettingUtil instance = new SettingUtil(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_copyright.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_description.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_github.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_history.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_info.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_menu_camera.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_menu_gallery.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_menu_manage.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_menu_send.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_menu_share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_menu_slideshow.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_person.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_photo_add.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_photo_color_chooser.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_photo_computer.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_photo_emotion.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_photo_financial.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_photo_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_photo_music.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_photo_share.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_photo_share_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/nav_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMeiji/Daily/ec4978f0e4b072544e3795b987baa1cfa43a97c6/app/src/main/res/drawable-v21/nav_header.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/error_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMeiji/Daily/ec4978f0e4b072544e3795b987baa1cfa43a97c6/app/src/main/res/drawable/error_image.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_cloud.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_photo_camera.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/side_nav_bar.xml: -------------------------------------------------------------------------------- 1 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_postscontent.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 25 | 26 | 36 | 37 | 43 | 44 | 45 | 46 | 47 | 52 | 53 | 57 | 58 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_postslist.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 24 | 25 | 26 | 27 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/container.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_useradd.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 19 | 20 | 27 | 28 | 32 | 33 | 34 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_zhuanlan.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_postlist.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 18 | 19 | 27 | 28 | 38 | 39 | 47 | 48 | 59 | 60 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_switch.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_zhuanlan.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 21 | 22 | 34 | 35 | 49 | 50 | 59 | 60 | 71 | 72 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /app/src/main/res/layout/nav_header_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/menu/activity_main_drawer.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 12 | 16 | 20 | 24 | 28 | 32 | 33 | 37 | 38 | 39 | 40 | 45 | 49 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMeiji/Daily/ec4978f0e4b072544e3795b987baa1cfa43a97c6/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMeiji/Daily/ec4978f0e4b072544e3795b987baa1cfa43a97c6/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMeiji/Daily/ec4978f0e4b072544e3795b987baa1cfa43a97c6/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMeiji/Daily/ec4978f0e4b072544e3795b987baa1cfa43a97c6/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMeiji/Daily/ec4978f0e4b072544e3795b987baa1cfa43a97c6/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #35464e 4 | #212a2f 5 | #212a2f 6 | #616161 7 | #212a2f 8 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #E91E63 4 | #C2185B 5 | #F8BBD0 6 | #FF5252 7 | #212121 8 | #757575 9 | #FFFFFF 10 | #BDBDBD 11 | 12 | #E91E63 13 | #C2185B 14 | #FF5252 15 | #616161 16 | @android:color/white 17 | 18 | #35464e 19 | #212a2f 20 | #212a2f 21 | #616161 22 | #212a2f 23 | 24 | #fafafa 25 | #F5F5F5 26 | #E0E0E0 27 | #bdbdbd 28 | #757575 29 | #424242 30 | #212121 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 160dp 5 | 6 | 16dp 7 | 16dp 8 | 16dp 9 | 240dp 10 | 16dp 11 | 10sp 12 | 240dp 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/drawables.xml: -------------------------------------------------------------------------------- 1 | 2 | @android:drawable/ic_menu_camera 3 | @android:drawable/ic_menu_gallery 4 | @android:drawable/ic_menu_slideshow 5 | @android:drawable/ic_menu_manage 6 | @android:drawable/ic_menu_share 7 | @android:drawable/ic_menu_send 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 知乎专栏 5 | 专栏 6 | 文章 7 | 添加专栏 8 | 关于 9 | 10 | 11 | Open navigation drawer 12 | Close navigation drawer 13 | 再按一次退出程序 14 | 15 | 16 | 产品·创业·互联网 17 | 生活·旅行·杂谈 18 | 音乐·影视·摄影 19 | 健康·情感·心理 20 | 专业领域 21 | 知乎·程序员 22 | 其它 23 | 自定义 24 | 切换主题颜色 25 | 关于 26 | 27 | 28 | 更新日志 29 | https://github.com/iMeiji/Daily/releases 30 | 主要开发者 31 | iMeiji 32 | 来瞧瞧这个:适配了质感设计的 知乎专栏! 33 | 源代码 34 | https://github.com/iMeiji/Daily 35 | 开源许可 36 | 版权声明 37 | 本App所使用的所有API均由 知乎(Zhihu.Inc) 提供 38 | 版本 39 | 分享至 40 | 版权声明 41 | 42 | 43 | 知道了 44 | 选择主题颜色 45 | 完成 46 | 取消 47 | 自定义 48 | 加载中 49 | 添加专栏ID 50 | 在下面输入专栏的ID,如 bilibili 51 | 什么是专栏id 52 | 确定 53 | 返回 54 | 55 | 56 | 57 | 格式不正确 58 | 已近添加过了 59 | 链接不正确 60 | 61 | 62 | 网络不给力 63 | 64 | 65 | 已删除 66 | 撤销 67 | 添加失败 68 | 添加成功 69 | 点击加号按钮添加专栏 \t 左右滑动可删除 70 | https://github.com/iMeiji/Daily/wiki/%E4%BB%80%E4%B9%88%E6%98%AF%E7%9F%A5%E4%B9%8E%E4%B8%93%E6%A0%8FID 71 | 72 | 73 | 没有更多了! 74 | 夜间模式 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 | 26 | 27 | 37 | 38 | 44 | 45 | 51 | 52 | 62 | 63 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/values/zhuanlan_ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | design 5 | pinapps 6 | WebNotes 7 | riobard 8 | luckystar 9 | zuimei 10 | hemingke 11 | bigertech 12 | smartdesigner 13 | 14 | 15 | 16 | zhihuadmin 17 | banquan 18 | zhihulive 19 | Weekly 20 | daily 21 | zhihumkt 22 | netjob 23 | FrontendMagazine 24 | 25 | 26 | 27 | he110world 28 | ciciatc 29 | japan-talk 30 | limiao 31 | hikarifromtokyo 32 | ethanlam 33 | hibetterme 34 | maboyong 35 | xiepanda 36 | niceliving 37 | uglypeoplefuckbooks 38 | doubtsinreading 39 | Wisdom 40 | oh-hard 41 | Song-Shibo 42 | 43 | 44 | 45 | lengai 46 | career 47 | aiqing 48 | lovemomo 49 | lswlsw 50 | happy 51 | psychology 52 | jiandanxinli 53 | knowyourself 54 | 55 | 56 | 57 | zhimovie 58 | Battlestar 59 | happymuyi 60 | 48zhen 61 | nordenbox 62 | classicalmusic 63 | DKLearnsPop 64 | klexraxmusic 65 | Photography 66 | 67 | 68 | 69 | mj1997 70 | transportation 71 | huizi 72 | agBJB 73 | delta 74 | chenqin 75 | finance 76 | jingjixue 77 | spoon 78 | wontfallinyourlap 79 | invest 80 | sailv 81 | wayneshiong 82 | 83 | 84 | -------------------------------------------------------------------------------- /app/src/release/java/com.meiji.daily/SdkManager.kt: -------------------------------------------------------------------------------- 1 | package com.meiji.daily 2 | 3 | import android.content.Context 4 | 5 | import okhttp3.OkHttpClient 6 | 7 | 8 | /** 9 | * Created by Meiji on 2018/1/19. 10 | */ 11 | 12 | object SdkManager { 13 | 14 | fun initStetho(context: Context) { 15 | } 16 | 17 | fun initInterceptor(builder: OkHttpClient.Builder): OkHttpClient.Builder { 18 | return builder 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.2.31' 5 | repositories { 6 | jcenter() 7 | google() 8 | maven { url 'https://maven.fabric.io/public' } 9 | mavenCentral() 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.1.1' 13 | classpath 'io.fabric.tools:gradle:1.25.2' 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 | classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0' 16 | // NOTE: Do not place your application dependencies here; they belong 17 | // in the individual module build.gradle files 18 | } 19 | } 20 | 21 | allprojects { 22 | repositories { 23 | jcenter() 24 | maven { url "https://jitpack.io" } 25 | google() 26 | maven { url 'https://maven.fabric.io/public' } 27 | mavenCentral() 28 | } 29 | 30 | tasks.withType(JavaCompile) { 31 | options.encoding = "UTF-8" 32 | } 33 | } 34 | 35 | task clean(type: Delete) { 36 | delete rootProject.buildDir 37 | } 38 | 39 | ext{ 40 | support_version = '27.1.0' 41 | arch_version = '1.1.1' 42 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ## Project-wide Gradle settings. 2 | # 3 | # For more details on how to configure your build environment visit 4 | # http://www.gradle.org/docs/current/userguide/build_environment.html 5 | # 6 | # Specifies the JVM arguments used for the daemon process. 7 | # The setting is particularly useful for tweaking memory settings. 8 | # Default value: -Xmx1024m -XX:MaxPermSize=256m 9 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 10 | # 11 | # When configured, Gradle will run in incubating parallel mode. 12 | # This option should only be used with decoupled projects. More details, visit 13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 14 | # org.gradle.parallel=true 15 | #Wed Nov 16 13:16:36 CST 2016 16 | org.gradle.jvmargs=-Xmx1536m -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMeiji/Daily/ec4978f0e4b072544e3795b987baa1cfa43a97c6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Nov 27 23:22:19 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.7-rc-1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start 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 start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /screenshots/daily_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMeiji/Daily/ec4978f0e4b072544e3795b987baa1cfa43a97c6/screenshots/daily_1.gif -------------------------------------------------------------------------------- /screenshots/daily_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMeiji/Daily/ec4978f0e4b072544e3795b987baa1cfa43a97c6/screenshots/daily_2.gif -------------------------------------------------------------------------------- /screenshots/daily_3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMeiji/Daily/ec4978f0e4b072544e3795b987baa1cfa43a97c6/screenshots/daily_3.gif -------------------------------------------------------------------------------- /screenshots/daily_4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMeiji/Daily/ec4978f0e4b072544e3795b987baa1cfa43a97c6/screenshots/daily_4.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------