├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release │ ├── app-release.apk │ └── output.json └── src │ ├── androidTest │ └── java │ │ └── me │ │ └── luowl │ │ └── wan │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── about_app.html │ ├── ic_launcher-web.png │ ├── java │ │ └── me │ │ │ └── luowl │ │ │ └── wan │ │ │ ├── AppConfig.kt │ │ │ ├── WanApplication.kt │ │ │ ├── base │ │ │ ├── AppManager.kt │ │ │ ├── ArticlePageListViewModel.kt │ │ │ ├── BaseActivity.kt │ │ │ ├── BaseFragment.kt │ │ │ ├── BaseViewModel.kt │ │ │ ├── ContainerActivity.kt │ │ │ ├── SingleLiveEvent.kt │ │ │ ├── StateModel.kt │ │ │ └── adapter │ │ │ │ ├── CommonFragmentStatePagerAdapter.kt │ │ │ │ ├── LoadMoreState.kt │ │ │ │ └── RVAdapter.kt │ │ │ ├── data │ │ │ ├── WanRepository.kt │ │ │ ├── local │ │ │ │ ├── SearchRecordDao.kt │ │ │ │ ├── SearchRecordDataSource.kt │ │ │ │ └── WanDatabase.kt │ │ │ ├── model │ │ │ │ ├── Architecture.kt │ │ │ │ ├── ArticleData.kt │ │ │ │ ├── ArticleTag.kt │ │ │ │ ├── BannerData.kt │ │ │ │ ├── BaseResp.kt │ │ │ │ ├── CoinData.kt │ │ │ │ ├── CoinPage.kt │ │ │ │ ├── LoginData.kt │ │ │ │ ├── Navigation.kt │ │ │ │ ├── PageData.kt │ │ │ │ ├── SearchHotKey.kt │ │ │ │ └── SearchRecord.kt │ │ │ └── network │ │ │ │ ├── ApiService.kt │ │ │ │ └── WanNetwork.kt │ │ │ ├── event │ │ │ ├── CollectEvent.kt │ │ │ └── LoginEvent.kt │ │ │ ├── http │ │ │ ├── ApiException.kt │ │ │ ├── CookieInterceptor.kt │ │ │ ├── HttpLoggingInterceptor.kt │ │ │ └── ServiceCreator.kt │ │ │ ├── ui │ │ │ ├── FindFragment.kt │ │ │ ├── FindViewModel.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainViewModel.kt │ │ │ ├── account │ │ │ │ ├── CoinListActivity.kt │ │ │ │ ├── CoinViewModel.kt │ │ │ │ ├── CollectionActivity.kt │ │ │ │ ├── CollectionViewModel.kt │ │ │ │ ├── LoginActivity.kt │ │ │ │ ├── LoginViewModel.kt │ │ │ │ ├── MineFragment.kt │ │ │ │ ├── MineViewModel.kt │ │ │ │ ├── RegisterActivity.kt │ │ │ │ └── RegisterViewModel.kt │ │ │ ├── architecture │ │ │ │ ├── ArchitectureArticleListFragment.kt │ │ │ │ ├── ArchitectureArticleListViewModel.kt │ │ │ │ ├── ArchitectureCategoryActivity.kt │ │ │ │ ├── ArchitectureCategoryViewModel.kt │ │ │ │ ├── ArchitectureFragment.kt │ │ │ │ └── ArchitectureViewModel.kt │ │ │ ├── home │ │ │ │ ├── HomeFragment.kt │ │ │ │ └── HomeViewModel.kt │ │ │ ├── navigation │ │ │ │ ├── NavigationFragment.kt │ │ │ │ └── NavigationViewModel.kt │ │ │ ├── project │ │ │ │ ├── ProjectFragment.kt │ │ │ │ ├── ProjectListFragment.kt │ │ │ │ ├── ProjectListViewModel.kt │ │ │ │ └── ProjectViewModel.kt │ │ │ ├── search │ │ │ │ ├── SearchActivity.kt │ │ │ │ └── SearchViewModel.kt │ │ │ ├── setting │ │ │ │ └── AboutFragment.kt │ │ │ ├── webview │ │ │ │ ├── SonicJavaScriptInterface.java │ │ │ │ ├── SonicRuntimeImpl.java │ │ │ │ ├── SonicSessionClientImpl.java │ │ │ │ ├── WebViewActivity.kt │ │ │ │ └── WebViewModel.kt │ │ │ └── wxarticle │ │ │ │ ├── ArticleListActivity.kt │ │ │ │ ├── ArticleListViewModel.kt │ │ │ │ ├── WXArticleChaptersFragment.kt │ │ │ │ └── WXArticleChaptersViewModel.kt │ │ │ ├── util │ │ │ ├── BindingAdapters.kt │ │ │ ├── BindingConverters.kt │ │ │ ├── GlobalUtil.kt │ │ │ ├── Log.kt │ │ │ ├── Preference.kt │ │ │ ├── ViewModelFactory.kt │ │ │ └── WanGlideModel.java │ │ │ └── widget │ │ │ ├── BannerView.java │ │ │ ├── RVDividerItemDecoration.java │ │ │ └── SimpleDividerItemDecoration.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── progress_bar.xml │ │ ├── selector_list_item_bg.xml │ │ ├── shape_knowledge_sub_child.xml │ │ ├── shape_login_edit_bg.xml │ │ ├── shape_search.xml │ │ ├── shape_top_article.xml │ │ └── shaper_fliter.xml │ │ ├── layout │ │ ├── activity_architecture_category.xml │ │ ├── activity_coin_list.xml │ │ ├── activity_collection.xml │ │ ├── activity_container.xml │ │ ├── activity_login.xml │ │ ├── activity_main.xml │ │ ├── activity_register.xml │ │ ├── activity_search.xml │ │ ├── activity_web_view.xml │ │ ├── activity_wx_article_list.xml │ │ ├── dialog_confirm_cannel_collection.xml │ │ ├── fragment_architecture.xml │ │ ├── fragment_architecture_article_list.xml │ │ ├── fragment_find.xml │ │ ├── fragment_home.xml │ │ ├── fragment_mine.xml │ │ ├── fragment_navigation.xml │ │ ├── fragment_project.xml │ │ ├── fragment_project_list.xml │ │ ├── fragment_wx_article_chapters.xml │ │ ├── nav_tab_item.xml │ │ ├── page_state_view.xml │ │ ├── recycler_item_architecture.xml │ │ ├── recycler_item_article.xml │ │ ├── recycler_item_banner.xml │ │ ├── recycler_item_coin.xml │ │ ├── recycler_item_collection_article.xml │ │ ├── recycler_item_entry.xml │ │ ├── recycler_item_filter.xml │ │ ├── recycler_item_knowledge_article.xml │ │ ├── recycler_item_load_more.xml │ │ ├── recycler_item_navigation_left_tab.xml │ │ ├── recycler_item_navigation_sub_child.xml │ │ ├── recycler_item_project.xml │ │ ├── recycler_item_search_record.xml │ │ ├── recycler_item_wx_article.xml │ │ ├── recycler_item_wx_chapter.xml │ │ └── recycler_view_header_view.xml │ │ ├── menu │ │ └── menu_search_view.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── bad_network_image.png │ │ ├── bg_mine_top.png │ │ ├── ic_about.png │ │ ├── ic_close.png │ │ ├── ic_coin.png │ │ ├── ic_collect.png │ │ ├── ic_collection.png │ │ ├── ic_feedback.png │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_round.png │ │ ├── ic_nav_home_normal.png │ │ ├── ic_nav_home_selected.png │ │ ├── ic_nav_mine_noraml.png │ │ ├── ic_nav_mine_selected.png │ │ ├── ic_nav_project_normal.png │ │ ├── ic_nav_project_selected.png │ │ ├── ic_nav_wx_chapters_normal.png │ │ ├── ic_nav_wx_chapters_selected.png │ │ ├── ic_record.png │ │ ├── ic_uncollect.png │ │ ├── ic_user_default_poster.png │ │ ├── icon_author.png │ │ ├── icon_search.png │ │ ├── icon_time.png │ │ ├── image_server_return_false.png │ │ ├── image_service_exception.png │ │ ├── no_content_image.png │ │ ├── user_default_img.png │ │ └── wan_android_logo.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── attr.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ids.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── me │ └── luowl │ └── wan │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── myapp.jks ├── screenshots ├── Screenshot_01.jpg ├── Screenshot_02.jpg ├── Screenshot_03.jpg ├── Screenshot_04.jpg ├── Screenshot_05.jpg ├── Screenshot_06.jpg ├── Screenshot_07.jpg ├── Screenshot_08.jpg ├── Screenshot_09.jpg └── Screenshot_10.jpg ├── settings.gradle └── test /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | /app/build 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 玩Android 练手app 2 | 3 | 使用Kotlin编写,利用玩Android开发[API](http://www.wanandroid.com/blog/show/2),开发的一个练手APP 4 | 5 | #### 主要完成的功能 6 | 7 | - 首页最新文章 8 | - 项目 9 | - 公众号 10 | - 体系 11 | - 网站导航 12 | - 登录、注册 13 | - 收藏 14 | - 文章搜索 15 | - 我的积分 16 | 17 | #### 相关技术 18 | 19 | - Kotlin 20 | - MVVM 21 | - DataBinding 22 | - Retrofit 23 | - Glide 24 | - Room 25 | 26 | 27 | #### 截图 28 | 29 | | - | - | - | 30 | |-------|:---:|-----------:| 31 | | | | | 32 | | | | | 33 | | | | | 34 | | 35 | 36 | 37 | #### 扫码下载 38 | 39 | ![下载地址](https://www.pgyer.com/app/qrcode/wanandroid4luowl) 40 | 41 | [点击下载1](https://www.pgyer.com/wanandroid4luowl) 42 | 43 | [点击下载2](https://github.com/luowl123/WanAndroid/blob/master/app/release/app-release.apk) 44 | 45 | #### 感谢 46 | 47 | 58 | 59 | #### 项目地址 60 | 61 | >https://github.com/luowl123/WanAndroid 62 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luowl123/WanAndroid/283d2cbb19786614163104c70df8ab3947e35ee8/app/release/app-release.apk -------------------------------------------------------------------------------- /app/release/output.json: -------------------------------------------------------------------------------- 1 | [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":3,"versionName":"1.0.2","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] -------------------------------------------------------------------------------- /app/src/androidTest/java/me/luowl/wan/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan 2 | 3 | import androidx.test.InstrumentationRegistry 4 | import androidx.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.open.wanandroid", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 42 | 43 | 44 | 49 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/assets/about_app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 关于APP 8 | 58 | 59 | 60 |

关于APP

61 | 62 |
63 |

64 | 只是一个练手app,使用玩Android开放API 65 |

66 |
67 |
1.功能概述

68 |

69 |

79 |

80 |
81 |
2.开源框架

82 |

83 |

94 |

95 |
96 |
3.关于作者

97 |

98 | https://github.com/luowl123/WanAndroid 99 |

100 |
101 |
4.未完待续

102 |

103 | loading... 104 |

105 |
106 | 107 |
108 |

      

109 | 110 | 111 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luowl123/WanAndroid/283d2cbb19786614163104c70df8ab3947e35ee8/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/AppConfig.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan 2 | 3 | import me.luowl.wan.data.model.LoginData 4 | import me.luowl.wan.util.Preference 5 | 6 | 7 | /* 8 | * 9 | * Created by luowl 10 | * Date: 2019/8/5 11 | * Desc: 12 | */ 13 | 14 | object AppConfig { 15 | 16 | const val SAVE_WAN_ANDROID_COOKIE = "save_wan_android_cookie" 17 | const val HEADER_SAVE_WAN_ANDROID_COOKIE = "save_wan_android_cookie:true" 18 | 19 | const val ADD_WAN_ANDROID_COOKIE = "add_wan_android_cookie" 20 | const val HEADER_AND_WAN_ANDROID_COOKIE = "add_wan_android_cookie:true" 21 | 22 | const val SET_COOKIE_KEY = "set-cookie" 23 | const val COOKIE_NAME = "Cookie" 24 | 25 | const val PROJECT_ISSUES_URL="https://github.com/luowl123/WanAndroid/issues" 26 | const val ABOUT_APP_URL="file:///android_asset/about_app.html" 27 | const val BUGLY_APP_ID="5448210acd"//自己申请 28 | 29 | fun encodeCookie(cookies: List): String { 30 | val sb = StringBuilder() 31 | val set = HashSet() 32 | cookies 33 | .map { cookie -> 34 | cookie.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() 35 | } 36 | .forEach { 37 | it.filterNot { set.contains(it) }.forEach { set.add(it) } 38 | } 39 | val ite = set.iterator() 40 | while (ite.hasNext()) { 41 | val cookie = ite.next() 42 | sb.append(cookie).append(";") 43 | } 44 | val last = sb.lastIndexOf(";") 45 | if (sb.length - 1 == last) { 46 | sb.deleteCharAt(last) 47 | } 48 | return sb.toString() 49 | } 50 | 51 | fun saveCookie(url: String?, domain: String?, cookies: String) { 52 | url ?: return 53 | var spUrl: String by Preference(url, cookies) 54 | @Suppress("UNUSED_VALUE") 55 | spUrl = cookies 56 | domain ?: return 57 | var spDomain: String by Preference(domain, cookies) 58 | @Suppress("UNUSED_VALUE") 59 | spDomain = cookies 60 | } 61 | 62 | const val LOGIN_KEY = "login" 63 | const val USERNAME_KEY = "username" 64 | const val PASSWORD_KEY = "password" 65 | const val TOKEN_KEY = "token" 66 | 67 | /** 68 | * local username 69 | */ 70 | var user: String by Preference(USERNAME_KEY, "") 71 | 72 | /** 73 | * local password 74 | */ 75 | var pwd: String by Preference(PASSWORD_KEY, "") 76 | 77 | /** 78 | * token 79 | */ 80 | var token: String by Preference(TOKEN_KEY, "") 81 | 82 | fun saveLoginInfo(info:LoginData){ 83 | user=info.username 84 | pwd=info.password 85 | token=info.token 86 | } 87 | 88 | fun clearLoginInfo(){ 89 | Preference.clearPreference() 90 | } 91 | 92 | fun isLogin(): Boolean { 93 | return user.isNotEmpty() 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/WanApplication.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.app.Application 6 | import android.content.Context 7 | import android.os.Bundle 8 | import android.util.Log 9 | import androidx.multidex.MultiDex 10 | import com.jeremyliao.liveeventbus.LiveEventBus 11 | import com.tencent.bugly.Bugly 12 | import com.tencent.sonic.sdk.SonicConfig 13 | import com.tencent.sonic.sdk.SonicEngine 14 | import me.luowl.wan.base.AppManager 15 | import me.luowl.wan.ui.webview.SonicRuntimeImpl 16 | import me.luowl.wan.util.logDebug 17 | import timber.log.Timber 18 | 19 | 20 | /** 21 | * 22 | * Created by luowl 23 | * Date: 2019/7/23 24 | * Desc: 25 | */ 26 | class WanApplication : Application() { 27 | 28 | override fun attachBaseContext(base: Context?) { 29 | super.attachBaseContext(base) 30 | MultiDex.install(this) 31 | } 32 | 33 | override fun onCreate() { 34 | super.onCreate() 35 | context = this 36 | 37 | val debug = BuildConfig.DEBUG 38 | if (debug) { 39 | Timber.plant(Timber.DebugTree()) 40 | } else { 41 | Timber.plant(CrashReportingTree()) 42 | } 43 | 44 | // CrashReport.initCrashReport(applicationContext, AppConfig.BUGLY_APP_ID, debug) 45 | Bugly.init(applicationContext, AppConfig.BUGLY_APP_ID, debug) 46 | 47 | LiveEventBus.get() 48 | .config() 49 | .supportBroadcast(this) 50 | .lifecycleObserverAlwaysActive(true) 51 | .autoClear(false) 52 | 53 | setApplication(this) 54 | 55 | // init sonic engine if necessary, or maybe u can do this when application created 56 | if (!SonicEngine.isGetInstanceAllowed()) { 57 | SonicEngine.createInstance(SonicRuntimeImpl(this), SonicConfig.Builder().build()) 58 | } 59 | 60 | } 61 | 62 | private fun setApplication(app: Application) { 63 | app.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { 64 | 65 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { 66 | logDebug(TAG, "${activity.javaClass.simpleName} onActivityCreated") 67 | AppManager.addActivity(activity) 68 | } 69 | 70 | override fun onActivityStarted(activity: Activity) { 71 | logDebug(TAG, "${activity.javaClass.simpleName} onActivityStarted") 72 | } 73 | 74 | override fun onActivityResumed(activity: Activity) { 75 | logDebug(TAG, "${activity.javaClass.simpleName} onActivityResumed") 76 | } 77 | 78 | override fun onActivityPaused(activity: Activity) { 79 | logDebug(TAG, "${activity.javaClass.simpleName} onActivityPaused") 80 | } 81 | 82 | override fun onActivityStopped(activity: Activity) { 83 | logDebug(TAG, "${activity.javaClass.simpleName} onActivityStopped") 84 | } 85 | 86 | override fun onActivityDestroyed(activity: Activity) { 87 | logDebug(TAG, "${activity.javaClass.simpleName} onActivityDestroyed") 88 | AppManager.finishActivity(activity) 89 | } 90 | 91 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { 92 | logDebug(TAG, "${activity.javaClass.simpleName} onActivitySaveInstanceState") 93 | } 94 | 95 | }) 96 | } 97 | 98 | class CrashReportingTree : Timber.Tree() { 99 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { 100 | if (priority == Log.VERBOSE || priority == Log.DEBUG) { 101 | return 102 | } 103 | } 104 | } 105 | 106 | companion object { 107 | const val TAG = "WanApplication" 108 | @SuppressLint("StaticFieldLeak") 109 | lateinit var context: Context 110 | } 111 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/base/ArticlePageListViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.base 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import me.luowl.wan.base.adapter.LoadMoreState 5 | import me.luowl.wan.data.WanRepository 6 | import me.luowl.wan.data.model.ArticleData 7 | import me.luowl.wan.data.model.BaseResp 8 | import me.luowl.wan.data.model.PageData 9 | import me.luowl.wan.util.GlobalUtil 10 | import java.util.* 11 | 12 | /* 13 | * 14 | * Created by luowl 15 | * Date: 2019/8/3 16 | * Desc: 17 | */ 18 | 19 | abstract class ArticlePageListViewModel constructor(val repository: WanRepository) : BaseViewModel() { 20 | private val _items = MutableLiveData>().apply { value = mutableListOf() } 21 | val dataList: MutableLiveData> = _items 22 | var pageIndex = 0 23 | var requestLoadDataState = MutableLiveData() 24 | 25 | abstract suspend fun request(): BaseResp 26 | 27 | override fun retry() { 28 | super.retry() 29 | pageIndex = 0 30 | getData() 31 | } 32 | 33 | fun getData() { 34 | launch({ 35 | if (pageIndex == 0) 36 | startLoading() 37 | val resp = request() 38 | checkResponseCode(resp) 39 | val pageData = resp.data ?: throw Throwable("page data is null") 40 | val pageItems = ArrayList(pageData.datas ?: listOf()) 41 | if (pageIndex == 0 || _items.value == null) { 42 | _items.value = pageItems 43 | } else { 44 | _items.value?.addAll(pageItems) 45 | } 46 | if (pageIndex == 0 && pageItems.size == 0) { 47 | loadDataEmpty() 48 | } else { 49 | loadDataFinish() 50 | } 51 | if (pageIndex < pageData.pageCount - 1) { 52 | pageIndex++ 53 | requestLoadDataState.value = LoadMoreState.STATE_LOAD_NONE 54 | } else { 55 | requestLoadDataState.value = LoadMoreState.STATE_LOAD_END 56 | } 57 | }, { 58 | if (pageIndex == 0) { 59 | loadDataError() 60 | } else { 61 | requestLoadDataState.value = LoadMoreState.STATE_LOAD_FAIL 62 | loadDataFinish() 63 | } 64 | }) 65 | } 66 | 67 | fun collectArticle(id: Long) { 68 | launch({ 69 | val resp = repository.addCollect(id) 70 | checkResponseCode(resp) 71 | // GlobalUtil.showToastShort("收藏成功") 72 | }, { 73 | // GlobalUtil.showToastShort("收藏失败") 74 | }) 75 | } 76 | 77 | fun cancelCollectArticle(id: Long) { 78 | launch({ 79 | val resp = repository.cancelCollectArticleByOriginId(id) 80 | checkResponseCode(resp) 81 | // GlobalUtil.showToastShort("取消收藏") 82 | }, { 83 | // GlobalUtil.showToastShort("操作失败") 84 | }) 85 | } 86 | 87 | fun cancelCollectArticle(id: Long, originId: Long) { 88 | launch({ 89 | val resp = repository.cancelCollectArticle(id, originId) 90 | checkResponseCode(resp) 91 | // GlobalUtil.showToastShort("取消收藏") 92 | retry() 93 | }, { 94 | // GlobalUtil.showToastShort("操作失败") 95 | }) 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/base/ContainerActivity.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.base 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.view.WindowManager 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.fragment.app.Fragment 8 | import me.luowl.wan.R 9 | import java.lang.ref.WeakReference 10 | 11 | 12 | /* 13 | * 14 | * Created by luowl 15 | * Date: 2019/8/6 16 | * Desc: 17 | */ 18 | 19 | class ContainerActivity : AppCompatActivity() { 20 | 21 | protected lateinit var mFragment: WeakReference 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) 25 | super.onCreate(savedInstanceState) 26 | setContentView(R.layout.activity_container) 27 | val fm = supportFragmentManager 28 | var fragment: Fragment? = null 29 | if (savedInstanceState != null) { 30 | fragment = fm.getFragment(savedInstanceState, FRAGMENT_TAG) 31 | } 32 | if (fragment == null) { 33 | fragment = initFromIntent(intent) 34 | } 35 | val trans = supportFragmentManager 36 | .beginTransaction() 37 | trans.replace(R.id.content, fragment) 38 | trans.commitAllowingStateLoss() 39 | mFragment = WeakReference(fragment) 40 | } 41 | 42 | override fun onSaveInstanceState(outState: Bundle) { 43 | super.onSaveInstanceState(outState) 44 | mFragment.get()?.let { supportFragmentManager.putFragment(outState, FRAGMENT_TAG, it) } 45 | } 46 | 47 | protected fun initFromIntent(data: Intent?): Fragment { 48 | if (data == null) { 49 | throw RuntimeException("you must provide a page info to display") 50 | } 51 | try { 52 | val fragmentName = data.getStringExtra(FRAGMENT) 53 | if (fragmentName == null || "" == fragmentName) { 54 | throw IllegalArgumentException("can not find page fragmentName") 55 | } 56 | val fragmentClass = Class.forName(fragmentName) 57 | val fragment = fragmentClass.newInstance() as Fragment 58 | val args = data.getBundleExtra(BUNDLE) 59 | if (args != null) { 60 | fragment.arguments = args 61 | } 62 | return fragment 63 | } catch (e: ClassNotFoundException) { 64 | e.printStackTrace() 65 | } catch (e: InstantiationException) { 66 | e.printStackTrace() 67 | } catch (e: IllegalAccessException) { 68 | e.printStackTrace() 69 | } 70 | 71 | throw RuntimeException("fragment initialization failed!") 72 | } 73 | 74 | override fun onBackPressed() { 75 | val fragment = supportFragmentManager.findFragmentById(R.id.content) 76 | if (fragment is BaseFragment<*, *>) { 77 | if (!fragment.isBackPressed()) { 78 | super.onBackPressed() 79 | } 80 | } else { 81 | super.onBackPressed() 82 | } 83 | } 84 | 85 | companion object { 86 | const val FRAGMENT_TAG = "content_fragment_tag" 87 | const val FRAGMENT = "fragment" 88 | const val BUNDLE = "bundle" 89 | } 90 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/base/SingleLiveEvent.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.base 2 | 3 | import androidx.annotation.MainThread 4 | import androidx.lifecycle.LifecycleOwner 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.Observer 7 | import java.util.concurrent.atomic.AtomicBoolean 8 | 9 | /* 10 | * 11 | * Created by luowl 12 | * Date: 2019/8/5 13 | * Desc: 14 | */ 15 | 16 | open class SingleLiveEvent : MutableLiveData() { 17 | 18 | private val pending = AtomicBoolean(false) 19 | 20 | @MainThread 21 | override fun setValue(value: T?) { 22 | pending.set(true) 23 | super.setValue(value) 24 | } 25 | 26 | @MainThread 27 | override fun observe(owner: LifecycleOwner, observer: Observer) { 28 | 29 | super.observe(owner, Observer { 30 | if (pending.compareAndSet(true, false)) { 31 | observer.onChanged(it) 32 | } 33 | }) 34 | } 35 | 36 | @MainThread 37 | fun call() { 38 | value = null 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/base/StateModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.base 2 | 3 | import androidx.databinding.BaseObservable 4 | 5 | /* 6 | * 7 | * Created by luowl 8 | * Date: 2019/8/2 9 | * Desc: 10 | */ 11 | 12 | class StateModel : BaseObservable() { 13 | 14 | private var state = STATE_NORMAL 15 | set(value) { 16 | field = value 17 | notifyChange() 18 | } 19 | 20 | val empty 21 | get() = state == STATE_EMPTY 22 | 23 | val loading 24 | get() = state == STATE_LOADING 25 | 26 | val error 27 | get() = state == STATE_ERROR 28 | 29 | val badNetwork 30 | get() = state == STATE_BAD_NETWORK 31 | 32 | val success 33 | get() = state == STATE_NORMAL 34 | 35 | fun showEmpty() { 36 | state = STATE_EMPTY 37 | } 38 | 39 | fun startLoading() { 40 | state = STATE_LOADING 41 | } 42 | 43 | fun loadDataError() { 44 | state = STATE_ERROR 45 | } 46 | 47 | fun loadDataFinish() { 48 | state = STATE_NORMAL 49 | } 50 | 51 | companion object { 52 | const val STATE_NORMAL = 0 53 | const val STATE_EMPTY = 1 54 | const val STATE_LOADING = 2 55 | const val STATE_ERROR = 3 56 | const val STATE_BAD_NETWORK = 4 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/base/adapter/CommonFragmentStatePagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.base.adapter 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentManager 5 | import androidx.fragment.app.FragmentStatePagerAdapter 6 | 7 | /* 8 | * 9 | * Created by luowl 10 | * Date: 2019/7/25 11 | * Desc: 12 | */ 13 | 14 | class CommonFragmentStatePagerAdapter( 15 | fm: FragmentManager, 16 | private val titles: List, 17 | private val fragments: List 18 | ) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { 19 | 20 | override fun getPageTitle(position: Int): CharSequence? { 21 | return titles[position] 22 | } 23 | 24 | override fun getCount(): Int { 25 | return titles.size 26 | } 27 | 28 | override fun getItem(position: Int): Fragment { 29 | return fragments[position] 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/base/adapter/LoadMoreState.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.base.adapter 2 | 3 | import androidx.databinding.BaseObservable 4 | 5 | /* 6 | * 7 | * Created by luowl 8 | * Date: 2019/7/24 9 | * Desc: 10 | */ 11 | 12 | class LoadMoreState : BaseObservable() { 13 | 14 | private var state = STATE_LOAD_NONE 15 | set(value) { 16 | field = value 17 | notifyChange() 18 | } 19 | 20 | val loading 21 | get() = state == STATE_LOADING 22 | 23 | val error 24 | get() = state == STATE_LOAD_FAIL 25 | 26 | val end 27 | get() = state == STATE_LOAD_END 28 | 29 | val none 30 | get() = state == STATE_LOAD_NONE 31 | 32 | fun loadMoreComplete() { 33 | state = STATE_LOAD_NONE 34 | } 35 | 36 | fun startLoading() { 37 | state = STATE_LOADING 38 | } 39 | 40 | fun loadMoreFail() { 41 | state = STATE_LOAD_FAIL 42 | } 43 | 44 | fun loadMoreEnd() { 45 | state = STATE_LOAD_END 46 | } 47 | 48 | companion object { 49 | public const val STATE_LOADING = 1 50 | public const val STATE_LOAD_FAIL = 2 51 | public const val STATE_LOAD_END = 3 52 | public const val STATE_LOAD_NONE = 4 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/local/SearchRecordDao.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.local 2 | 3 | import androidx.room.* 4 | import me.luowl.wan.data.model.SearchRecord 5 | 6 | /* 7 | * 8 | * Created by luowl 9 | * Date: 2019/7/30 10 | * Desc: 11 | */ 12 | @Dao 13 | interface SearchRecordDao { 14 | @Query("SELECT * FROM search_records order by record_id desc") 15 | suspend fun getRecords(): List 16 | 17 | @Insert 18 | suspend fun insertRecord(record: SearchRecord) 19 | 20 | @Delete 21 | suspend fun deleteRecord(record: SearchRecord) 22 | 23 | @Query("SELECT * FROM search_records WHERE keyword=:keyword") 24 | suspend fun findRecord(keyword: String): List 25 | 26 | @Query("DELETE FROM search_records WHERE keyword=:keyword") 27 | suspend fun deleteRecords(keyword: String) 28 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/local/SearchRecordDataSource.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.local 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.withContext 6 | import me.luowl.wan.data.model.SearchRecord 7 | 8 | /* 9 | * 10 | * Created by luowl 11 | * Date: 2019/7/30 12 | * Desc: 13 | */ 14 | 15 | class SearchRecordDataSource internal constructor( 16 | private val recordDao: SearchRecordDao, 17 | private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO 18 | ) { 19 | suspend fun getRecords(): List = withContext(ioDispatcher) { 20 | recordDao.getRecords() 21 | } 22 | 23 | suspend fun insertRecord(record: SearchRecord) = withContext(ioDispatcher) { 24 | recordDao.insertRecord(record) 25 | } 26 | 27 | suspend fun deleteRecord(record: SearchRecord) = withContext(ioDispatcher) { 28 | recordDao.deleteRecord(record) 29 | } 30 | 31 | suspend fun findRecord(keyword: String): List = withContext(ioDispatcher) { 32 | recordDao.findRecord(keyword) 33 | } 34 | 35 | suspend fun deleteRecords(keyword: String)= withContext(ioDispatcher){ 36 | recordDao.deleteRecords(keyword) 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/local/WanDatabase.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.local 2 | 3 | import androidx.room.Database 4 | import androidx.room.Room 5 | import androidx.room.RoomDatabase 6 | import me.luowl.wan.WanApplication 7 | import me.luowl.wan.data.model.SearchRecord 8 | 9 | /* 10 | * 11 | * Created by luowl 12 | * Date: 2019/7/30 13 | * Desc: 14 | */ 15 | @Database(entities = [SearchRecord::class], version = 2, exportSchema = false) 16 | abstract 17 | class WanDatabase : RoomDatabase() { 18 | abstract fun searchRecordDao(): SearchRecordDao 19 | 20 | companion object { 21 | 22 | val instance = Single.sin 23 | 24 | } 25 | 26 | private object Single { 27 | 28 | val sin: WanDatabase = Room.databaseBuilder( 29 | WanApplication.context, 30 | WanDatabase::class.java, 31 | "Wan.db" 32 | ) 33 | .allowMainThreadQueries() 34 | .build() 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/model/Architecture.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.model 2 | 3 | import java.io.Serializable 4 | 5 | /** 6 | * 7 | * Created by luowl 8 | * Date: 2019/7/23 9 | * Desc: 10 | */ 11 | class Architecture :Serializable{ 12 | /** 13 | "courseId": 13, 14 | "id": 150, 15 | "name": "开发环境", 16 | "order": 1, 17 | "parentChapterId": 0, 18 | "userControlSetTop": false, 19 | "visible": 1, 20 | "children":[] 21 | * */ 22 | 23 | var courseId = 0 24 | var id = 0L 25 | var name = "" 26 | var order = 0 27 | var parentChapterId = 0 28 | var userControlSetTop = false 29 | var visible = 1 30 | var children: List? = null 31 | 32 | fun getChildrenNames(): String { 33 | if (children == null) return "" 34 | return children!!.joinToString(separator = "\t",transform = {it.name}) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/model/ArticleData.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.model 2 | 3 | /** 4 | * 5 | * Created by luowl 6 | * Date: 2019/7/23 7 | * Desc: 8 | */ 9 | class ArticleData { 10 | /** 11 | { 12 | "apkLink": "", 13 | "author": " 振之", 14 | "chapterId": 26, 15 | "chapterName": "基础UI控件", 16 | "collect": false, 17 | "courseId": 13, 18 | "desc": "", 19 | "envelopePic": "", 20 | "fresh": false, 21 | "id": 8575, 22 | "link": "https://juejin.im/post/597d88f75188257fc2177c36", 23 | "niceDate": "2019-06-03", 24 | "origin": "", 25 | "prefix": "", 26 | "projectLink": "", 27 | "publishTime": 1559534523000, 28 | "superChapterId": 26, 29 | "superChapterName": "常用控件", 30 | "tags": [], 31 | "title": "如何实现 “中间这几个字要加粗,但是不要太粗,比较纤细的那种粗” ?", 32 | "type": 0, 33 | "userId": -1, 34 | "visible": 1, 35 | "zan": 0 36 | } 37 | */ 38 | var apkLink = "" 39 | var author = "" 40 | var chapterId = 0 41 | var chapterName = "" 42 | var collect = false 43 | var courseId = 0 44 | var desc = "" 45 | var envelopePic = "" 46 | var fresh = false 47 | var id = 0L 48 | var link = "" 49 | var niceDate = "" 50 | get() { 51 | return "时间:$field" 52 | } 53 | var origin = "" 54 | var originId=0L 55 | var prefix = "" 56 | var projectLink = "" 57 | var publishTime = 0L 58 | var superChapterId = 0 59 | var superChapterName = "" 60 | var title = "" 61 | var type = 0 62 | var userId = -1 63 | var visible = 1 64 | var zan = 0L 65 | 66 | var tags: MutableList? = null 67 | 68 | fun getChapter(): String { 69 | if (superChapterName.isNotEmpty()) 70 | return "$superChapterName/$chapterName" 71 | return chapterName 72 | } 73 | 74 | fun hasDesc():Boolean{ 75 | return desc.isNotEmpty() 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/model/ArticleTag.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.model 2 | 3 | /** 4 | * 5 | * Created by luowl 6 | * Date: 2019/7/23 7 | * Desc: 8 | */ 9 | class ArticleTag { 10 | var name = "" 11 | var url = "" 12 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/model/BannerData.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.model 2 | 3 | /** 4 | * 5 | * Created by luowl 6 | * Date: 2019/7/23 7 | * Desc: 8 | */ 9 | class BannerData { 10 | 11 | /** 12 | { 13 | "desc": "Android高级进阶直播课免费学习", 14 | "id": 22, 15 | "imagePath": "https://wanandroid.com/blogimgs/fbed8f14-1043-4a43-a7ee-0651996f7c49.jpeg", 16 | "isVisible": 1, 17 | "order": 0, 18 | "title": "Android高级进阶直播课免费学习", 19 | "type": 0, 20 | "url": "https://url.163.com/4bj" 21 | } 22 | */ 23 | 24 | var desc = "" 25 | var id = 0L 26 | var imagePath = "" 27 | var isVibile = 1 28 | var order = 0 29 | var title = "" 30 | var type = 0 31 | var url = "" 32 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/model/BaseResp.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.model 2 | 3 | /** 4 | * 5 | * Created by luowl 6 | * Date: 2019/7/23 7 | * Desc: 8 | */ 9 | open class BaseResp { 10 | var errorCode = 0 11 | 12 | var errorMsg = "" 13 | 14 | var data: T? = null 15 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/model/CoinData.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.model 2 | 3 | import java.io.Serializable 4 | 5 | /* 6 | * 7 | * Created by luowl 8 | * Date: 2019/8/31 9 | * Desc: 10 | */ 11 | 12 | class CoinData :Serializable{ 13 | var coinCount = 0L 14 | var date = 0L 15 | var desc = "" 16 | var id = 0L 17 | var type = 0 18 | var userId = 0L 19 | var userName = "" 20 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/model/CoinPage.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import java.io.Serializable 5 | 6 | /* 7 | * 8 | * Created by luowl 9 | * Date: 2019/8/31 10 | * Desc: 11 | */ 12 | 13 | class CoinPage :Serializable{ 14 | @SerializedName("curPage") 15 | var currentPage = 0 16 | var datas: MutableList? = null 17 | var offset = 0 18 | var over = false 19 | var pageCount = 0 20 | var size = 0 21 | var total = 0L 22 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/model/LoginData.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.model 2 | 3 | import java.io.Serializable 4 | 5 | /* 6 | * 7 | * Created by luowl 8 | * Date: 2019/8/5 9 | * Desc: 10 | */ 11 | 12 | class LoginData : Serializable { 13 | var admin = false 14 | var email = "" 15 | var icon = "" 16 | var id = 0L 17 | var nickname = "" 18 | var password = "" 19 | var token = "" 20 | var type = 0 21 | var username = "" 22 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/model/Navigation.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.model 2 | 3 | /** 4 | * 5 | * Created by luowl 6 | * Date: 2019/7/23 7 | * Desc: 8 | */ 9 | class Navigation { 10 | var articles: List? = null 11 | var cid = 0 12 | var name = "" 13 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/model/PageData.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import java.io.Serializable 5 | 6 | /** 7 | * 8 | * Created by luowl 9 | * Date: 2019/7/23 10 | * Desc: 11 | */ 12 | class PageData :Serializable{ 13 | @SerializedName("curPage") 14 | var currentPage = 0 15 | var datas: MutableList? = null 16 | var offset = 0 17 | var over = false 18 | var pageCount = 0 19 | var size = 0 20 | var total = 0L 21 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/model/SearchHotKey.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.model 2 | 3 | /** 4 | * 5 | * Created by luowl 6 | * Date: 2019/7/23 7 | * Desc: 8 | */ 9 | class SearchHotKey { 10 | /** 11 | { 12 | "id": 6, 13 | "link": "", 14 | "name": "面试", 15 | "order": 1, 16 | "visible": 1 17 | } 18 | */ 19 | 20 | var id = 0L 21 | var link = "" 22 | var name = "" 23 | var order = 0 24 | var visible = 1 25 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/model/SearchRecord.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.model 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.Ignore 6 | import androidx.room.PrimaryKey 7 | 8 | /* 9 | * 10 | * Created by luowl 11 | * Date: 2019/7/23 12 | * Desc: 13 | */ 14 | @Entity(tableName = "search_records") 15 | class SearchRecord { 16 | 17 | @PrimaryKey(autoGenerate = true) 18 | @ColumnInfo(name = "record_id") 19 | var id: Long = 0L 20 | 21 | @ColumnInfo(name = "keyword") 22 | var keyword: String = "" 23 | 24 | @Ignore 25 | constructor() 26 | 27 | constructor( 28 | keyword: String = "", 29 | id: Long = 0L 30 | ) { 31 | this.id = id 32 | this.keyword = keyword 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/data/network/WanNetwork.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.data.network 2 | 3 | import me.luowl.wan.http.ServiceCreator 4 | import retrofit2.Call 5 | import retrofit2.Callback 6 | import retrofit2.Response 7 | import kotlin.coroutines.resume 8 | import kotlin.coroutines.resumeWithException 9 | import kotlin.coroutines.suspendCoroutine 10 | 11 | /** 12 | * 13 | * Created by luowl 14 | * Date: 2019/7/23 15 | * Desc: 16 | */ 17 | class WanNetwork { 18 | 19 | private val apiService = ServiceCreator.create(ApiService::class.java) 20 | 21 | suspend fun getHomeArticleList(pageIndex: Int) = apiService.getHomeArticleList(pageIndex).await() 22 | 23 | suspend fun getBanner() = apiService.getBanner().await() 24 | 25 | suspend fun getTopArticleList() = apiService.getTopArticleList().await() 26 | 27 | suspend fun getHotSearchKey() = apiService.getHotSearchKey().await() 28 | 29 | suspend fun getArchitectureTree() = apiService.getArchitectureTree().await() 30 | 31 | suspend fun getArticleListByTreeId(pageIndex: Int, cid: Long) = 32 | apiService.getArticleListByTreeId(pageIndex, cid).await() 33 | 34 | suspend fun getNavigationData() = apiService.getNavigationData().await() 35 | 36 | suspend fun getProjectClassification() = apiService.getProjectClassification().await() 37 | 38 | suspend fun getProjectList(pageIndex: Int, cid: Long) = apiService.getProjectList(pageIndex, cid).await() 39 | 40 | suspend fun getNewestProjectList(pageIndex: Int) = apiService.getNewestProjectList(pageIndex).await() 41 | 42 | suspend fun getWXArticleChapters() = apiService.getWXArticleChapters().await() 43 | 44 | suspend fun getWXArticleList(chapterId: Long, pageIndex: Int) = 45 | apiService.getWXArticleList(chapterId, pageIndex).await() 46 | 47 | suspend fun getWXArticleListByKey(chapterId: Long, pageIndex: Int, key: String) = 48 | apiService.getWXArticleListByKey(chapterId, pageIndex, key).await() 49 | 50 | suspend fun searchArticleByKey(pageIndex: Int, key: String) = apiService.searchArticleByKey(pageIndex, key).await() 51 | 52 | suspend fun login(username:String,password:String)=apiService.login(username,password).await() 53 | 54 | suspend fun register(username:String,password:String,repassword:String)=apiService.register(username,password,repassword).await() 55 | 56 | suspend fun logout()=apiService.logout().await() 57 | 58 | suspend fun getCollectList(pageIndex: Int)=apiService.getCollectList(pageIndex).await() 59 | 60 | suspend fun addCollect(articleId:Long)=apiService.addCollectArticle(articleId).await() 61 | 62 | suspend fun addCollectOutsideArticle(title: String,author: String,link: String)=apiService.addCollectOutsideArticle(title,author,link).await() 63 | 64 | suspend fun cancelCollectArticleByOriginId(articleId:Long)=apiService.cancelCollectArticleByOriginId(articleId).await() 65 | 66 | suspend fun cancelCollectArticle(articleId:Long,originId: Long=-1)=apiService.cancelCollectArticle(articleId,originId).await() 67 | 68 | suspend fun getMyCoinCount() = apiService.getMyCoinCount().await() 69 | 70 | suspend fun getCoinList(pageIndex: Int) = apiService.getCoinList(pageIndex).await() 71 | 72 | private suspend fun Call.await(): T { 73 | return suspendCoroutine { continuation -> 74 | enqueue(object : Callback { 75 | override fun onFailure(call: Call, t: Throwable) { 76 | continuation.resumeWithException(t) 77 | } 78 | 79 | override fun onResponse(call: Call, response: Response) { 80 | val body = response.body() 81 | if (body != null) continuation.resume(body) 82 | else continuation.resumeWithException(RuntimeException("response body is null")) 83 | } 84 | }) 85 | } 86 | } 87 | 88 | companion object { 89 | 90 | private var network: WanNetwork? = null 91 | 92 | fun getInstance(): WanNetwork { 93 | if (network == null) { 94 | synchronized(WanNetwork::class.java) { 95 | if (network == null) { 96 | network = WanNetwork() 97 | } 98 | } 99 | } 100 | return network!! 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/event/CollectEvent.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.event 2 | 3 | /* 4 | * 5 | * Created by luowl 6 | * Date: 2019/8/8 7 | * Desc: 8 | */ 9 | 10 | class CollectEvent() -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/event/LoginEvent.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.event 2 | 3 | /* 4 | * 5 | * Created by luowl 6 | * Date: 2019/8/5 7 | * Desc: 8 | */ 9 | 10 | class LoginEvent(val success: Boolean) -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/http/ApiException.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.http 2 | 3 | /* 4 | * 5 | * Created by luowl 6 | * Date: 2019/8/6 7 | * Desc: 8 | */ 9 | 10 | class ApiException(val errorCode:Int,val errorMsg:String) : Throwable(errorMsg) { 11 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/http/CookieInterceptor.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.http 2 | 3 | import me.luowl.wan.AppConfig 4 | import me.luowl.wan.util.Preference 5 | import me.luowl.wan.util.logDebug 6 | import okhttp3.Interceptor 7 | import okhttp3.Response 8 | 9 | /** 10 | * @desc CookieInterceptor: 添加Cookie,保存Cookie 11 | */ 12 | class CookieInterceptor : Interceptor { 13 | 14 | override fun intercept(chain: Interceptor.Chain): Response { 15 | 16 | val request = chain.request() 17 | val builder = request.newBuilder() 18 | 19 | val addCookie = request.header(AppConfig.ADD_WAN_ANDROID_COOKIE)?.equals("true") ?: false 20 | logDebug("addCookie:$addCookie") 21 | 22 | builder.addHeader("Content-type", "application/json; charset=utf-8") 23 | 24 | val domain = request.url().host() 25 | val requestUrl = request.url().toString() 26 | if (domain.isNotEmpty() && addCookie) { 27 | val spDomain: String by Preference(domain, "") 28 | val cookie: String = if (spDomain.isNotEmpty()) spDomain else "" 29 | if (cookie.isNotEmpty()) { 30 | // 将 Cookie 添加到请求头 31 | builder.addHeader(AppConfig.COOKIE_NAME, cookie) 32 | } 33 | } 34 | 35 | val response = chain.proceed(builder.build()) 36 | val saveCookie = request.header(AppConfig.SAVE_WAN_ANDROID_COOKIE)?.equals("true") ?: false 37 | logDebug("saveCookie:$saveCookie") 38 | // set-cookie maybe has multi, login to save cookie 39 | if (saveCookie && response.headers(AppConfig.SET_COOKIE_KEY).isNotEmpty()) { 40 | val cookies = response.headers(AppConfig.SET_COOKIE_KEY) 41 | val cookie = AppConfig.encodeCookie(cookies) 42 | AppConfig.saveCookie(requestUrl, domain, cookie) 43 | } 44 | 45 | return response 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/http/ServiceCreator.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.http 2 | 3 | import me.luowl.wan.BuildConfig 4 | import okhttp3.OkHttpClient 5 | import retrofit2.Retrofit 6 | import retrofit2.converter.gson.GsonConverterFactory 7 | import retrofit2.converter.scalars.ScalarsConverterFactory 8 | import java.util.logging.Level 9 | 10 | object ServiceCreator { 11 | 12 | private const val BASE_URL = "https://www.wanandroid.com/" 13 | 14 | private val httpClient = OkHttpClient.Builder() 15 | 16 | init { 17 | val loggingInterceptor = HttpLoggingInterceptor("WanHttp") 18 | loggingInterceptor.setPrintLevel(if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE) 19 | loggingInterceptor.setColorLevel(Level.INFO) 20 | httpClient.addInterceptor(CookieInterceptor()) 21 | httpClient.addInterceptor(loggingInterceptor) 22 | } 23 | 24 | private val builder = Retrofit.Builder() 25 | .baseUrl(BASE_URL) 26 | .client(httpClient.build()) 27 | .addConverterFactory(ScalarsConverterFactory.create()) 28 | .addConverterFactory(GsonConverterFactory.create()) 29 | 30 | 31 | private val retrofit = builder.build() 32 | 33 | fun create(serviceClass: Class): T = retrofit.create(serviceClass) 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/FindFragment.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui 2 | 3 | import android.os.Bundle 4 | import android.widget.TextView 5 | import androidx.fragment.app.Fragment 6 | import androidx.viewpager.widget.ViewPager 7 | import kotlinx.android.synthetic.main.fragment_find.* 8 | import me.luowl.wan.BR 9 | import me.luowl.wan.R 10 | import me.luowl.wan.base.BaseFragment 11 | import me.luowl.wan.base.adapter.CommonFragmentStatePagerAdapter 12 | import me.luowl.wan.databinding.FragmentFindBinding 13 | import me.luowl.wan.ui.architecture.ArchitectureFragment 14 | import me.luowl.wan.ui.navigation.NavigationFragment 15 | import me.luowl.wan.ui.wxarticle.WXArticleChaptersFragment 16 | import me.luowl.wan.util.GlobalUtil 17 | import me.luowl.wan.util.logDebug 18 | 19 | /* 20 | * 21 | * Created by luowl 22 | * Date: 2019/7/26 23 | * Desc: 24 | */ 25 | 26 | class FindFragment : BaseFragment() { 27 | 28 | override fun getViewModelClass(): Class = FindViewModel::class.java 29 | 30 | override fun getLayoutId() = R.layout.fragment_find 31 | 32 | override fun initVariableId() = BR.viewModel 33 | 34 | var currentIndex = 0 35 | val tabs: ArrayList = ArrayList() 36 | 37 | override fun setupViews(savedInstanceState: Bundle?) { 38 | super.setupViews(savedInstanceState) 39 | val titles = listOf( 40 | GlobalUtil.getString(R.string.chapter), 41 | GlobalUtil.getString(R.string.system), 42 | GlobalUtil.getString(R.string.navigation) 43 | ) 44 | tabs.addAll(listOf(tv_item_wx_chapters, tv_item_system, tv_item_guide)) 45 | val fragments = ArrayList() 46 | fragments.add(WXArticleChaptersFragment()) 47 | fragments.add(ArchitectureFragment()) 48 | fragments.add(NavigationFragment()) 49 | setItemSelected(tabs[currentIndex]) 50 | view_pager.adapter = CommonFragmentStatePagerAdapter(childFragmentManager, titles, fragments) 51 | view_pager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { 52 | override fun onPageScrollStateChanged(state: Int) { 53 | } 54 | 55 | override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { 56 | } 57 | 58 | override fun onPageSelected(position: Int) { 59 | logDebug("select $position") 60 | setItemUnSelected(tabs[currentIndex]) 61 | setItemSelected(tabs[position]) 62 | currentIndex = position 63 | } 64 | }) 65 | setItemClickEvent() 66 | } 67 | 68 | private fun setItemClickEvent() { 69 | tv_item_wx_chapters.setOnClickListener { 70 | view_pager.currentItem = 0 71 | } 72 | tv_item_system.setOnClickListener { 73 | view_pager.currentItem = 1 74 | } 75 | tv_item_guide.setOnClickListener { 76 | view_pager.currentItem = 2 77 | } 78 | } 79 | 80 | fun setItemSelected(textView: TextView) { 81 | textView.setTextColor(GlobalUtil.getColor(R.color.white)) 82 | } 83 | 84 | fun setItemUnSelected(textView: TextView) { 85 | textView.setTextColor(GlobalUtil.getColor(R.color.half_white)) 86 | } 87 | 88 | fun setCurrentPage(index:Int){ 89 | view_pager.currentItem = index 90 | } 91 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/FindViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui 2 | 3 | import me.luowl.wan.base.BaseViewModel 4 | 5 | /* 6 | * 7 | * Created by luowl 8 | * Date: 2019/8/3 9 | * Desc: 10 | */ 11 | 12 | class FindViewModel :BaseViewModel(){ 13 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui 2 | 3 | import androidx.lifecycle.ViewModel 4 | 5 | /** 6 | * 7 | * Created by luowl 8 | * Date: 2019/7/23 9 | * Desc: 10 | */ 11 | class MainViewModel : ViewModel() { 12 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/account/CoinListActivity.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.account 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.lifecycle.Observer 7 | import androidx.recyclerview.widget.LinearLayoutManager 8 | import me.luowl.wan.BR 9 | import me.luowl.wan.R 10 | import me.luowl.wan.base.BaseActivity 11 | import me.luowl.wan.base.adapter.LoadMoreState 12 | import me.luowl.wan.base.adapter.RVAdapter 13 | import me.luowl.wan.data.model.CoinData 14 | import me.luowl.wan.databinding.ActivityCoinListBinding 15 | import me.luowl.wan.util.logDebug 16 | import me.luowl.wan.widget.SimpleDividerItemDecoration 17 | 18 | /* 19 | * 20 | * Created by luowl 21 | * Date: 2019/8/31 22 | * Desc: 23 | */ 24 | 25 | class CoinListActivity : BaseActivity() { 26 | 27 | private lateinit var adapter: RVAdapter 28 | 29 | override fun getViewModelClass(): Class = CoinViewModel::class.java 30 | 31 | override fun getLayoutId(): Int = R.layout.activity_coin_list 32 | 33 | override fun initVariableId(): Int = BR.viewModel 34 | 35 | override fun setupViews(savedInstanceState: Bundle?) { 36 | super.setupViews(savedInstanceState) 37 | setupToolbar() 38 | binding.recyclerView.layoutManager = LinearLayoutManager(this) 39 | binding.recyclerView.addItemDecoration(SimpleDividerItemDecoration(this, 1f)) 40 | adapter = 41 | object : RVAdapter(mutableListOf(), R.layout.recycler_item_coin, BR.data) { 42 | } 43 | adapter.setRequestLoadMoreListener(object : RVAdapter.LoadMoreListener { 44 | override fun loadMore() { 45 | viewModel.getData() 46 | } 47 | }, binding.recyclerView) 48 | binding.recyclerView.adapter = adapter 49 | } 50 | 51 | override fun initViewObservable() { 52 | super.initViewObservable() 53 | viewModel.dataList.observe(this, Observer { 54 | logDebug("dataChange") 55 | adapter.setData(viewModel.dataList.value) 56 | }) 57 | viewModel.requestLoadDataState.observe(this, Observer { 58 | when (it) { 59 | LoadMoreState.STATE_LOAD_FAIL -> adapter.loadMoreFail() 60 | LoadMoreState.STATE_LOAD_END -> adapter.loadMoreEnd() 61 | LoadMoreState.STATE_LOAD_NONE -> adapter.loadMoreComplete() 62 | } 63 | }) 64 | } 65 | 66 | override fun startLoadData() { 67 | super.startLoadData() 68 | viewModel.dataList.value?.run { 69 | if (isEmpty()) { 70 | viewModel.getData() 71 | } 72 | } 73 | } 74 | 75 | companion object { 76 | fun startActivity(context: Context) { 77 | context.startActivity(Intent(context, CoinListActivity::class.java)) 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/account/CoinViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.account 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import me.luowl.wan.base.BaseViewModel 5 | import me.luowl.wan.base.adapter.LoadMoreState 6 | import me.luowl.wan.data.WanRepository 7 | import me.luowl.wan.data.model.* 8 | import java.util.ArrayList 9 | 10 | /* 11 | * 12 | * Created by luowl 13 | * Date: 2019/8/31 14 | * Desc: 15 | */ 16 | 17 | class CoinViewModel constructor(val repository: WanRepository) :BaseViewModel(){ 18 | private val _items = MutableLiveData>().apply { value = mutableListOf() } 19 | val dataList: MutableLiveData> = _items 20 | var pageIndex = 0 21 | var requestLoadDataState = MutableLiveData() 22 | 23 | override fun retry() { 24 | super.retry() 25 | pageIndex = 0 26 | getData() 27 | } 28 | 29 | fun getData() { 30 | launch({ 31 | if (pageIndex == 0) 32 | startLoading() 33 | val resp = repository.getCoinList(pageIndex) 34 | checkResponseCode(resp) 35 | val pageData = resp.data ?: throw Throwable("page data is null") 36 | val pageItems = ArrayList(pageData.datas ?: listOf()) 37 | if (pageIndex == 0 || _items.value == null) { 38 | _items.value = pageItems 39 | } else { 40 | _items.value?.addAll(pageItems) 41 | } 42 | if (pageIndex == 0 && pageItems.size == 0) { 43 | loadDataEmpty() 44 | } else { 45 | loadDataFinish() 46 | } 47 | if (pageIndex < pageData.pageCount - 1) { 48 | pageIndex++ 49 | requestLoadDataState.value = LoadMoreState.STATE_LOAD_NONE 50 | } else { 51 | requestLoadDataState.value = LoadMoreState.STATE_LOAD_END 52 | } 53 | }, { 54 | if (pageIndex == 0) { 55 | loadDataError() 56 | } else { 57 | requestLoadDataState.value = LoadMoreState.STATE_LOAD_FAIL 58 | loadDataFinish() 59 | } 60 | }) 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/account/CollectionViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.account 2 | 3 | import me.luowl.wan.base.ArticlePageListViewModel 4 | import me.luowl.wan.data.WanRepository 5 | import me.luowl.wan.data.model.BaseResp 6 | import me.luowl.wan.data.model.PageData 7 | 8 | /* 9 | * 10 | * Created by luowl 11 | * Date: 2019/8/6 12 | * Desc: 13 | */ 14 | 15 | class CollectionViewModel constructor(repository: WanRepository) : ArticlePageListViewModel(repository) { 16 | 17 | override suspend fun request(): BaseResp = repository.getCollectionList(pageIndex) 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/account/LoginActivity.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.account 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.lifecycle.Observer 7 | import com.jeremyliao.liveeventbus.LiveEventBus 8 | import me.luowl.wan.AppConfig 9 | import me.luowl.wan.R 10 | import me.luowl.wan.BR 11 | import me.luowl.wan.base.BaseActivity 12 | import me.luowl.wan.databinding.ActivityLoginBinding 13 | import me.luowl.wan.event.LoginEvent 14 | 15 | /* 16 | * 17 | * Created by luowl 18 | * Date: 2019/8/5 19 | * Desc: 20 | */ 21 | 22 | class LoginActivity : BaseActivity() { 23 | override fun getViewModelClass(): Class = LoginViewModel::class.java 24 | 25 | override fun getLayoutId() = R.layout.activity_login 26 | 27 | override fun initVariableId() = BR.viewModel 28 | 29 | override fun setupViews(savedInstanceState: Bundle?) { 30 | super.setupViews(savedInstanceState) 31 | setupToolbar() 32 | binding.registerBtn.setOnClickListener { 33 | RegisterActivity.startActivity(this@LoginActivity) 34 | } 35 | } 36 | 37 | override fun initViewObservable() { 38 | super.initViewObservable() 39 | LiveEventBus.get().with(AppConfig.LOGIN_KEY, LoginEvent::class.java) 40 | .observe(this, Observer { 41 | finish() 42 | }) 43 | } 44 | 45 | companion object { 46 | 47 | fun startActivity(context: Context) { 48 | val intent = Intent(context, LoginActivity::class.java) 49 | context.startActivity(intent) 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/account/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.account 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import com.jeremyliao.liveeventbus.LiveEventBus 5 | import me.luowl.wan.AppConfig 6 | import me.luowl.wan.base.BaseViewModel 7 | import me.luowl.wan.data.WanRepository 8 | import me.luowl.wan.event.LoginEvent 9 | import me.luowl.wan.util.GlobalUtil 10 | import me.luowl.wan.util.logDebug 11 | 12 | /* 13 | * 14 | * Created by luowl 15 | * Date: 2019/8/5 16 | * Desc: 17 | */ 18 | 19 | class LoginViewModel constructor(private val repository: WanRepository) : BaseViewModel() { 20 | 21 | val username: MutableLiveData = MutableLiveData() 22 | val password: MutableLiveData = MutableLiveData() 23 | 24 | fun login() { 25 | logDebug("username=${username.value} password=${password.value}") 26 | val usernameValue = username.value 27 | val pwdValue = password.value 28 | if (usernameValue.isNullOrEmpty()) { 29 | GlobalUtil.showToastShort("请输入用户名") 30 | return 31 | } 32 | if (pwdValue.isNullOrEmpty()) { 33 | GlobalUtil.showToastShort("请输入密码") 34 | return 35 | } 36 | launch({ 37 | val resp = repository.login(usernameValue, pwdValue) 38 | when (resp.errorCode) { 39 | 0 -> { 40 | showDialog("登录成功") 41 | AppConfig.saveLoginInfo(resp.data!!) 42 | postLoginSuccessEvent() 43 | // onBackPressed() 44 | } 45 | else -> { 46 | showDialog("登录失败【${resp.errorMsg}】") 47 | } 48 | } 49 | }, {}) 50 | } 51 | 52 | private fun postLoginSuccessEvent(){ 53 | LiveEventBus.get().with(AppConfig.LOGIN_KEY).post(LoginEvent(true)) 54 | } 55 | 56 | 57 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/account/MineFragment.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.account 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AlertDialog 5 | import androidx.lifecycle.Observer 6 | import com.jeremyliao.liveeventbus.LiveEventBus 7 | import me.luowl.wan.AppConfig 8 | import me.luowl.wan.BR 9 | import me.luowl.wan.R 10 | import me.luowl.wan.base.BaseFragment 11 | import me.luowl.wan.databinding.FragmentMineBinding 12 | import me.luowl.wan.event.LoginEvent 13 | import me.luowl.wan.ui.webview.WebViewActivity 14 | import me.luowl.wan.util.GlobalUtil 15 | 16 | /* 17 | * 18 | * Created by luowl 19 | * Date: 2019/8/4 20 | * Desc: 21 | */ 22 | 23 | class MineFragment : BaseFragment() { 24 | override fun getViewModelClass(): Class = MineViewModel::class.java 25 | 26 | override fun getLayoutId() = R.layout.fragment_mine 27 | 28 | override fun initVariableId() = BR.viewModel 29 | 30 | override fun setupViews(savedInstanceState: Bundle?) { 31 | super.setupViews(savedInstanceState) 32 | updateInfo() 33 | binding.collectionView.setOnClickListener { 34 | if (viewModel.isLogin()) { 35 | context?.let { ctx -> CollectionActivity.startActivity(ctx) } 36 | } else { 37 | showLoginDialog() 38 | } 39 | } 40 | 41 | binding.feedbackView.setOnClickListener { 42 | context?.let { 43 | WebViewActivity.startActivity( 44 | it, 45 | GlobalUtil.getString(R.string.text_feedback), 46 | AppConfig.PROJECT_ISSUES_URL 47 | ) 48 | } 49 | } 50 | 51 | binding.aboutView.setOnClickListener { 52 | context?.let { 53 | WebViewActivity.startActivity( 54 | it, 55 | GlobalUtil.getString(R.string.text_about), 56 | AppConfig.ABOUT_APP_URL 57 | ) 58 | } 59 | } 60 | 61 | binding.coinView.setOnClickListener { 62 | if (viewModel.isLogin()) { 63 | context?.let { ctx -> CoinListActivity.startActivity(ctx) } 64 | } else { 65 | showLoginDialog() 66 | } 67 | } 68 | 69 | binding.logoutBtn.setOnClickListener { 70 | showConfirmLogoutDialog() 71 | } 72 | } 73 | 74 | private fun updateInfo() { 75 | if (AppConfig.user.isNotEmpty()) { 76 | binding.usernameTv.text = AppConfig.user 77 | binding.userImg.setOnClickListener { 78 | } 79 | viewModel.getMyCoin() 80 | } else { 81 | binding.usernameTv.text = "未登录" 82 | binding.userImg.setOnClickListener { 83 | context?.let { 84 | LoginActivity.startActivity(it) 85 | } 86 | } 87 | } 88 | } 89 | 90 | override fun initLoginChangeObservable() { 91 | LiveEventBus.get().with(AppConfig.LOGIN_KEY, LoginEvent::class.java) 92 | .observe(this, Observer { 93 | updateInfo() 94 | refreshLayout() 95 | }) 96 | } 97 | 98 | private fun showConfirmLogoutDialog() { 99 | context?.let { 100 | val dialog = AlertDialog.Builder(it).setTitle(GlobalUtil.getString(R.string.logout_tip)).setPositiveButton( 101 | GlobalUtil.getString(R.string.logout_yes) 102 | ) { dialog, _ -> 103 | dialog.dismiss() 104 | viewModel.logout() 105 | }.setNegativeButton(GlobalUtil.getString(R.string.logout_no)) { dialog, _ -> 106 | dialog.dismiss() 107 | } 108 | dialog.show() 109 | } 110 | } 111 | 112 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/account/MineViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.account 2 | 3 | import com.jeremyliao.liveeventbus.LiveEventBus 4 | import me.luowl.wan.AppConfig 5 | import me.luowl.wan.base.BaseViewModel 6 | import me.luowl.wan.base.SingleLiveEvent 7 | import me.luowl.wan.data.WanRepository 8 | import me.luowl.wan.event.LoginEvent 9 | 10 | /* 11 | * 12 | * Created by luowl 13 | * Date: 2019/8/4 14 | * Desc: 15 | */ 16 | 17 | class MineViewModel constructor(private val repository: WanRepository) : BaseViewModel() { 18 | 19 | val myCoinCount = SingleLiveEvent().apply { value = "我的积分" } 20 | val showMyCoin = SingleLiveEvent().apply { value = false } 21 | 22 | fun getMyCoin() { 23 | launch({ 24 | val resp = repository.getMyCoinCount() 25 | checkResponseCode(resp) 26 | myCoinCount.value="我的积分:${resp.data}" 27 | showMyCoin.setValue(true) 28 | }, { 29 | showMyCoin.setValue(false) 30 | }) 31 | } 32 | 33 | fun isLogin(): Boolean { 34 | return AppConfig.isLogin() 35 | } 36 | 37 | fun logout() { 38 | launch({ 39 | repository.logout() 40 | clearData() 41 | }, { 42 | clearData() 43 | }) 44 | } 45 | 46 | private fun clearData() { 47 | showMyCoin.value = false 48 | AppConfig.clearLoginInfo() 49 | LiveEventBus.get().with(AppConfig.LOGIN_KEY).post(LoginEvent(false)) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/account/RegisterActivity.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.account 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import me.luowl.wan.BR 7 | import me.luowl.wan.R 8 | import me.luowl.wan.base.BaseActivity 9 | import me.luowl.wan.databinding.ActivityRegisterBinding 10 | 11 | /* 12 | * 13 | * Created by luowl 14 | * Date: 2019/8/6 15 | * Desc: 16 | */ 17 | 18 | class RegisterActivity : BaseActivity() { 19 | override fun getViewModelClass(): Class = RegisterViewModel::class.java 20 | 21 | override fun getLayoutId(): Int = R.layout.activity_register 22 | 23 | override fun initVariableId(): Int = BR.viewModel 24 | 25 | override fun setupViews(savedInstanceState: Bundle?) { 26 | super.setupViews(savedInstanceState) 27 | setupToolbar() 28 | } 29 | 30 | companion object { 31 | 32 | fun startActivity(context: Context) { 33 | val intent = Intent(context, RegisterActivity::class.java) 34 | context.startActivity(intent) 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/account/RegisterViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.account 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import com.jeremyliao.liveeventbus.LiveEventBus 5 | import me.luowl.wan.AppConfig 6 | import me.luowl.wan.base.BaseViewModel 7 | import me.luowl.wan.data.WanRepository 8 | import me.luowl.wan.event.LoginEvent 9 | import me.luowl.wan.util.GlobalUtil 10 | import me.luowl.wan.util.logDebug 11 | import java.util.regex.Pattern 12 | 13 | /* 14 | * 15 | * Created by luowl 16 | * Date: 2019/8/6 17 | * Desc: 18 | */ 19 | 20 | class RegisterViewModel constructor(private val repository: WanRepository) : BaseViewModel() { 21 | 22 | val username: MutableLiveData = MutableLiveData() 23 | val password: MutableLiveData = MutableLiveData() 24 | val repassword: MutableLiveData = MutableLiveData() 25 | 26 | 27 | fun register() { 28 | // logDebug("username=${username.value} password=${password.value}") 29 | val usernameValue = username.value 30 | val pwdValue = password.value 31 | val rePwdValue = repassword.value 32 | if (usernameValue.isNullOrEmpty()) { 33 | GlobalUtil.showToastShort("请输入用户名") 34 | return 35 | } 36 | if (usernameValue.length < 6) { 37 | GlobalUtil.showToastShort("用户名最少6位") 38 | return 39 | } 40 | if (pwdValue.isNullOrEmpty()) { 41 | GlobalUtil.showToastShort("请输入密码") 42 | return 43 | } 44 | if (!regexPwd(pwdValue)) { 45 | GlobalUtil.showToastShort("密码6~50位且为数字、字母、-、_") 46 | return 47 | } 48 | if (rePwdValue.isNullOrEmpty()) { 49 | GlobalUtil.showToastShort("请输入确认密码") 50 | return 51 | } 52 | if (!pwdValue.equals(rePwdValue, false)) { 53 | GlobalUtil.showToastShort("确认密码和密码不符") 54 | return 55 | } 56 | launch({ 57 | val resp = repository.register(usernameValue, pwdValue, rePwdValue) 58 | when (resp.errorCode) { 59 | 0 -> { 60 | showDialog("注册成功") 61 | AppConfig.saveLoginInfo(resp.data!!) 62 | postLoginSuccessEvent() 63 | finish() 64 | } 65 | else -> { 66 | showDialog("注册失败【${resp.errorMsg}】") 67 | } 68 | } 69 | }, { 70 | 71 | }) 72 | } 73 | 74 | private fun postLoginSuccessEvent() { 75 | LiveEventBus.get().with(AppConfig.LOGIN_KEY).post(LoginEvent(true)) 76 | } 77 | 78 | private fun regexPwd(pwd: String): Boolean { 79 | //密码6~50位且为数字、字母、-、_ 80 | val regex = "^[A-Za-z0-9_-]{6,50}\$" 81 | return Pattern.matches(regex, pwd) 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/architecture/ArchitectureArticleListViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.architecture 2 | 3 | import me.luowl.wan.base.ArticlePageListViewModel 4 | import me.luowl.wan.data.WanRepository 5 | import me.luowl.wan.data.model.BaseResp 6 | import me.luowl.wan.data.model.PageData 7 | 8 | /* 9 | * 10 | * Created by luowl 11 | * Date: 2019/7/29 12 | * Desc: 13 | */ 14 | 15 | class ArchitectureArticleListViewModel constructor(repository: WanRepository) : ArticlePageListViewModel(repository) { 16 | 17 | var treeId: Long = 0L 18 | 19 | override suspend fun request(): BaseResp = repository.getArticleListByTreeId(pageIndex, treeId) 20 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/architecture/ArchitectureCategoryActivity.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.architecture 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.fragment.app.Fragment 7 | import kotlinx.android.synthetic.main.activity_architecture_category.* 8 | import me.luowl.wan.BR 9 | import me.luowl.wan.R 10 | import me.luowl.wan.base.BaseActivity 11 | import me.luowl.wan.base.adapter.CommonFragmentStatePagerAdapter 12 | import me.luowl.wan.data.model.Architecture 13 | import me.luowl.wan.databinding.ActivityArchitectureCategoryBinding 14 | 15 | /* 16 | * 17 | * Created by luowl 18 | * Date: 2019/7/29 19 | * Desc: 20 | */ 21 | 22 | class ArchitectureCategoryActivity : 23 | BaseActivity() { 24 | 25 | override fun getViewModelClass(): Class = ArchitectureCategoryViewModel::class.java 26 | 27 | override fun getLayoutId() = R.layout.activity_architecture_category 28 | 29 | override fun initVariableId() = BR.viewModel 30 | 31 | override fun setupViews(savedInstanceState: Bundle?) { 32 | super.setupViews(savedInstanceState) 33 | setupToolbar() 34 | val data = intent.getBundleExtra("data") 35 | data?.let { 36 | setUpTabLayout(data.getSerializable("architecture") as Architecture) 37 | } 38 | } 39 | 40 | private fun setUpTabLayout(data: Architecture) { 41 | title = data.name 42 | val fragments = ArrayList() 43 | val titles = ArrayList() 44 | for (item in data.children!!) { 45 | titles.add(item.name) 46 | fragments.add(ArchitectureArticleListFragment.newInstance(item.id)) 47 | } 48 | view_pager.adapter = CommonFragmentStatePagerAdapter(supportFragmentManager, titles, fragments) 49 | tab_layout.setupWithViewPager(view_pager) 50 | } 51 | 52 | companion object { 53 | fun startActivity(context: Context, data: Architecture) { 54 | val intent = Intent(context, ArchitectureCategoryActivity::class.java).apply { 55 | val bundle = Bundle() 56 | bundle.putSerializable("architecture", data) 57 | putExtra("data", bundle) 58 | } 59 | context.startActivity(intent) 60 | } 61 | } 62 | 63 | 64 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/architecture/ArchitectureCategoryViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.architecture 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import me.luowl.wan.base.BaseViewModel 5 | import me.luowl.wan.data.model.Architecture 6 | 7 | /* 8 | * 9 | * Created by luowl 10 | * Date: 2019/7/29 11 | * Desc: 12 | */ 13 | 14 | class ArchitectureCategoryViewModel :BaseViewModel(){ 15 | 16 | var architecture: MutableLiveData? = null 17 | 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/architecture/ArchitectureFragment.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.architecture 2 | 3 | import android.os.Bundle 4 | import androidx.databinding.ViewDataBinding 5 | import androidx.lifecycle.Observer 6 | import androidx.recyclerview.widget.LinearLayoutManager 7 | import kotlinx.android.synthetic.main.fragment_architecture.* 8 | import me.luowl.wan.BR 9 | import me.luowl.wan.R 10 | import me.luowl.wan.base.BaseFragment 11 | import me.luowl.wan.base.adapter.RVAdapter 12 | import me.luowl.wan.data.model.Architecture 13 | import me.luowl.wan.databinding.FragmentArchitectureBinding 14 | import me.luowl.wan.widget.SimpleDividerItemDecoration 15 | 16 | /* 17 | * 18 | * Created by luowl 19 | * Date: 2019/7/27 20 | * Desc: 21 | */ 22 | 23 | class ArchitectureFragment : BaseFragment() { 24 | 25 | 26 | private lateinit var adapter: RVAdapter 27 | 28 | override fun getViewModelClass(): Class = ArchitectureViewModel::class.java 29 | 30 | override fun getLayoutId() = R.layout.fragment_architecture 31 | 32 | override fun initVariableId() = BR.viewModel 33 | 34 | override fun setupViews(savedInstanceState: Bundle?) { 35 | super.setupViews(savedInstanceState) 36 | val layoutManager = LinearLayoutManager(context) 37 | recycler_view.layoutManager = layoutManager 38 | recycler_view.addItemDecoration(SimpleDividerItemDecoration(context, 10f)) 39 | 40 | adapter = object : 41 | RVAdapter(mutableListOf(), R.layout.recycler_item_architecture, BR.data) { 42 | override fun addListener(binding: ViewDataBinding, itemData: Architecture, position: Int) { 43 | binding.root.setOnClickListener { 44 | context?.run { 45 | ArchitectureCategoryActivity.startActivity(this, itemData) 46 | } 47 | } 48 | } 49 | } 50 | recycler_view.adapter = adapter 51 | } 52 | 53 | override fun initViewObservable() { 54 | super.initViewObservable() 55 | viewModel.dataList.observe(this, Observer { 56 | adapter.setData(it) 57 | }) 58 | } 59 | 60 | override fun startLoadData() { 61 | super.startLoadData() 62 | viewModel.getData() 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/architecture/ArchitectureViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.architecture 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import me.luowl.wan.base.BaseViewModel 5 | import me.luowl.wan.data.WanRepository 6 | import me.luowl.wan.data.model.Architecture 7 | 8 | /* 9 | * 10 | * Created by luowl 11 | * Date: 2019/7/27 12 | * Desc: 13 | */ 14 | 15 | class ArchitectureViewModel constructor(private val repository: WanRepository) : BaseViewModel() { 16 | 17 | 18 | private val _items = MutableLiveData>().apply { value = mutableListOf() } 19 | val dataList: MutableLiveData> = _items 20 | 21 | var leftTabIndex = MutableLiveData() 22 | 23 | fun getData() { 24 | launch({ 25 | stateModel.startLoading() 26 | val resp = repository.getArchitectureTree() 27 | val data = resp.data ?: listOf() 28 | if (data.isNotEmpty()) { 29 | _items.value = ArrayList(data) 30 | leftTabIndex.value = 0 31 | stateModel.loadDataFinish() 32 | } else { 33 | stateModel.showEmpty() 34 | } 35 | }, { 36 | stateModel.loadDataError() 37 | }) 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/navigation/NavigationViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.navigation 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import me.luowl.wan.base.BaseViewModel 5 | import me.luowl.wan.data.WanRepository 6 | import me.luowl.wan.data.model.Navigation 7 | 8 | /* 9 | * 10 | * Created by luowl 11 | * Date: 2019/7/27 12 | * Desc: 13 | */ 14 | 15 | class NavigationViewModel constructor(private val repository: WanRepository) : BaseViewModel() { 16 | 17 | private val _items = MutableLiveData>().apply { value = listOf() } 18 | val dataList: MutableLiveData> = _items 19 | val leftTabIndex = MutableLiveData() 20 | 21 | fun getData() { 22 | launch({ 23 | stateModel.startLoading() 24 | val resp = repository.getNavigationData() 25 | val data = resp.data ?: listOf() 26 | if (data.isNotEmpty()) { 27 | _items.value = data 28 | leftTabIndex.value = 0 29 | stateModel.loadDataFinish() 30 | } else { 31 | stateModel.showEmpty() 32 | } 33 | }, { 34 | stateModel.loadDataError() 35 | }) 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/project/ProjectFragment.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.project 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.lifecycle.Observer 5 | import kotlinx.android.synthetic.main.fragment_project.* 6 | import me.luowl.wan.BR 7 | import me.luowl.wan.R 8 | import me.luowl.wan.base.BaseFragment 9 | import me.luowl.wan.base.adapter.CommonFragmentStatePagerAdapter 10 | import me.luowl.wan.data.model.Architecture 11 | import me.luowl.wan.databinding.FragmentProjectBinding 12 | 13 | /* 14 | * 15 | * Created by luowl 16 | * Date: 2019/7/25 17 | * Desc: 18 | */ 19 | 20 | class ProjectFragment : BaseFragment() { 21 | 22 | override fun getViewModelClass(): Class = ProjectViewModel::class.java 23 | 24 | override fun getLayoutId() = R.layout.fragment_project 25 | 26 | override fun initVariableId() = BR.viewModel 27 | 28 | override fun initViewObservable() { 29 | super.initViewObservable() 30 | viewModel.dataList.observe(this, Observer { 31 | setUpTabLayout(it) 32 | }) 33 | } 34 | 35 | override fun startLoadData() { 36 | super.startLoadData() 37 | viewModel.dataList.value?.run { 38 | if (isEmpty()) { 39 | viewModel.getData() 40 | } 41 | } 42 | } 43 | 44 | private fun setUpTabLayout(items: List) { 45 | val titles = ArrayList() 46 | val fragments = ArrayList() 47 | for (item in items) { 48 | titles.add(item.name) 49 | fragments.add(ProjectListFragment.newInstance(item.id)) 50 | } 51 | view_pager.adapter = 52 | CommonFragmentStatePagerAdapter(childFragmentManager, titles, fragments) 53 | tab_layout.setupWithViewPager(view_pager) 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/project/ProjectListFragment.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.project 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.databinding.ViewDataBinding 6 | import androidx.lifecycle.Observer 7 | import androidx.recyclerview.widget.LinearLayoutManager 8 | import kotlinx.android.synthetic.main.fragment_project_list.* 9 | import me.luowl.wan.BR 10 | import me.luowl.wan.R 11 | import me.luowl.wan.base.BaseFragment 12 | import me.luowl.wan.base.adapter.LoadMoreState 13 | import me.luowl.wan.base.adapter.RVAdapter 14 | import me.luowl.wan.data.model.ArticleData 15 | import me.luowl.wan.databinding.FragmentProjectListBinding 16 | import me.luowl.wan.ui.webview.WebViewActivity 17 | import me.luowl.wan.util.logDebug 18 | import me.luowl.wan.widget.SimpleDividerItemDecoration 19 | 20 | /* 21 | * 22 | * Created by luowl 23 | * Date: 2019/7/25 24 | * Desc: 25 | */ 26 | 27 | class ProjectListFragment : BaseFragment() { 28 | 29 | private lateinit var adapter: RVAdapter 30 | 31 | override fun getViewModelClass(): Class = ProjectListViewModel::class.java 32 | 33 | override fun getLayoutId() = R.layout.fragment_project_list 34 | 35 | override fun initVariableId() = BR.viewModel 36 | 37 | override fun initParams() { 38 | super.initParams() 39 | arguments?.let { 40 | viewModel.treeId = it.getLong(EXTRA_PROJECT_TREE_ID) 41 | } 42 | } 43 | 44 | override fun setupViews(savedInstanceState: Bundle?) { 45 | super.setupViews(savedInstanceState) 46 | swipe_refresh_layout.setColorSchemeColors(resources.getColor(R.color.appThemeColor)) 47 | recycler_view.layoutManager = LinearLayoutManager(context) 48 | recycler_view.addItemDecoration(SimpleDividerItemDecoration(context, 10f)) 49 | adapter = object : RVAdapter(mutableListOf(), R.layout.recycler_item_project, BR.article) { 50 | override fun addListener(binding: ViewDataBinding, itemData: ArticleData, position: Int) { 51 | binding.root.setOnClickListener { 52 | context?.let { 53 | WebViewActivity.startActivity(it, itemData.title, itemData.link) 54 | } 55 | } 56 | } 57 | } 58 | adapter.setRequestLoadMoreListener(object : RVAdapter.LoadMoreListener { 59 | override fun loadMore() { 60 | viewModel.getData() 61 | } 62 | }, recycler_view) 63 | recycler_view.adapter = adapter 64 | } 65 | 66 | override fun initViewObservable() { 67 | super.initViewObservable() 68 | viewModel.dataList.observe(this, Observer { 69 | logDebug("dataChange") 70 | adapter.setData(viewModel.dataList.value) 71 | }) 72 | viewModel.requestLoadDataState.observe(this, Observer { 73 | when (it) { 74 | LoadMoreState.STATE_LOAD_FAIL -> adapter.loadMoreFail() 75 | LoadMoreState.STATE_LOAD_END -> adapter.loadMoreEnd() 76 | LoadMoreState.STATE_LOAD_NONE -> adapter.loadMoreComplete() 77 | } 78 | }) 79 | } 80 | 81 | override fun startLoadData() { 82 | super.startLoadData() 83 | viewModel.dataList.value?.run { 84 | if (isEmpty()) { 85 | viewModel.getData() 86 | } 87 | } 88 | } 89 | 90 | companion object { 91 | const val EXTRA_PROJECT_TREE_ID = "treeId" 92 | fun newInstance(treeId: Long): ProjectListFragment { 93 | val fragment = ProjectListFragment() 94 | val data = Bundle() 95 | data.putLong(EXTRA_PROJECT_TREE_ID, treeId) 96 | fragment.arguments = data 97 | return fragment 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/project/ProjectListViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.project 2 | 3 | import me.luowl.wan.base.ArticlePageListViewModel 4 | import me.luowl.wan.data.WanRepository 5 | import me.luowl.wan.data.model.BaseResp 6 | import me.luowl.wan.data.model.PageData 7 | 8 | /* 9 | * 10 | * Created by luowl 11 | * Date: 2019/7/25 12 | * Desc: 13 | */ 14 | 15 | class ProjectListViewModel(repository: WanRepository) : ArticlePageListViewModel(repository) { 16 | 17 | var treeId: Long = 0L 18 | 19 | override suspend fun request(): BaseResp { 20 | return if (treeId == -1L) repository.getNewestProjectList(pageIndex) else repository.getProjectList( 21 | pageIndex, 22 | treeId 23 | ) 24 | } 25 | 26 | // fun getData() { 27 | // launch({ 28 | // if (pageIndex == 0) 29 | // stateModel.startLoading() 30 | // val resp = if (treeId == -1L) repository.getNewestProjectList(pageIndex) else repository.getProjectList( 31 | // pageIndex, 32 | // treeId 33 | // ) 34 | // when (resp.errorCode) { 35 | // 0 -> { 36 | // val pageData = resp.data ?: throw Throwable("page data is null") 37 | // val pageItems = ArrayList(pageData.datas ?: listOf()) 38 | // if (_items.value == null) { 39 | // _items.value = pageItems 40 | // } else { 41 | // _items.value?.addAll(pageItems) 42 | // } 43 | // if (pageIndex == 0 && pageItems.size == 0) { 44 | // stateModel.showEmpty() 45 | // } else { 46 | // stateModel.loadDataFinish() 47 | // } 48 | // if (pageIndex < pageData.pageCount - 1) { 49 | // pageIndex++ 50 | // requestLoadDataState.value = LoadMoreState.STATE_LOAD_NONE 51 | // } else { 52 | // requestLoadDataState.value = LoadMoreState.STATE_LOAD_END 53 | // } 54 | // } 55 | // else -> { 56 | // if (pageIndex == 0) { 57 | // stateModel.loadDataError() 58 | // } else { 59 | // requestLoadDataState.value = LoadMoreState.STATE_LOAD_FAIL 60 | // stateModel.loadDataFinish() 61 | // } 62 | // } 63 | // } 64 | // }, { 65 | // if (pageIndex == 0) { 66 | // stateModel.loadDataError() 67 | // } else { 68 | // requestLoadDataState.value = LoadMoreState.STATE_LOAD_FAIL 69 | // stateModel.loadDataFinish() 70 | // } 71 | // }) 72 | // } 73 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/project/ProjectViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.project 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import me.luowl.wan.base.BaseViewModel 5 | import me.luowl.wan.data.WanRepository 6 | import me.luowl.wan.data.model.Architecture 7 | 8 | /* 9 | * 10 | * Created by luowl 11 | * Date: 2019/7/25 12 | * Desc: 13 | */ 14 | 15 | class ProjectViewModel constructor(private val repository: WanRepository) : BaseViewModel() { 16 | 17 | private val _items = MutableLiveData>().apply { value = mutableListOf() } 18 | val dataList: MutableLiveData> = _items 19 | 20 | fun getData() { 21 | launch({ 22 | stateModel.startLoading() 23 | val resp = repository.getProjectClassification() 24 | val tempItems = ArrayList() 25 | val newProjectItem = Architecture() 26 | newProjectItem.id = -1 27 | newProjectItem.name = "最新" 28 | tempItems.add(newProjectItem) 29 | resp.data?.let { 30 | if (it.isNotEmpty()) { 31 | tempItems.addAll(it) 32 | } 33 | } 34 | _items.value = tempItems 35 | stateModel.loadDataFinish() 36 | }, { 37 | stateModel.loadDataError() 38 | }) 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/search/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.search 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import me.luowl.wan.base.ArticlePageListViewModel 6 | import me.luowl.wan.data.WanRepository 7 | import me.luowl.wan.data.local.SearchRecordDataSource 8 | import me.luowl.wan.data.model.BaseResp 9 | import me.luowl.wan.data.model.PageData 10 | import me.luowl.wan.data.model.SearchRecord 11 | 12 | /* 13 | * 14 | * Created by luowl 15 | * Date: 2019/7/29 16 | * Desc: 17 | */ 18 | 19 | class SearchViewModel constructor( 20 | repository: WanRepository, 21 | private val localDataSource: SearchRecordDataSource 22 | ) : ArticlePageListViewModel(repository) { 23 | 24 | private val _records = MutableLiveData>().apply { value = mutableListOf() } 25 | val records: LiveData> = _records 26 | 27 | var keyword: String? = null 28 | 29 | fun queryRecord() { 30 | launch { 31 | _records.value = localDataSource.getRecords() 32 | } 33 | } 34 | 35 | fun deleteRecord(keyword: String) { 36 | launch { 37 | localDataSource.deleteRecords(keyword) 38 | queryRecord() 39 | } 40 | } 41 | 42 | private fun insertRecord(keyword: String) { 43 | launch({ 44 | val oldRecords = localDataSource.findRecord(keyword) 45 | if (oldRecords.isNotEmpty()) { 46 | localDataSource.deleteRecords(keyword) 47 | } 48 | val newRecord = SearchRecord(keyword) 49 | localDataSource.insertRecord(newRecord) 50 | }, {}) 51 | } 52 | 53 | fun doSearch(key: String) { 54 | insertRecord(key) 55 | keyword = key 56 | retry() 57 | } 58 | 59 | override suspend fun request(): BaseResp = repository.searchArticleByKey(pageIndex, keyword!!) 60 | 61 | fun searchArticleByKey() { 62 | getData() 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/setting/AboutFragment.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.setting 2 | 3 | import androidx.fragment.app.Fragment 4 | 5 | /* 6 | * 7 | * Created by luowl 8 | * Date: 2019/8/26 9 | * Desc: 10 | */ 11 | 12 | class AboutFragment :Fragment(){ 13 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/webview/SonicJavaScriptInterface.java: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.webview; 2 | 3 | import android.content.Intent; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | import android.webkit.JavascriptInterface; 7 | import com.tencent.sonic.sdk.SonicDiffDataCallback; 8 | import org.json.JSONObject; 9 | 10 | /** 11 | * Sonic javaScript Interface (Android API Level >= 17) 12 | */ 13 | 14 | public class SonicJavaScriptInterface { 15 | 16 | private final SonicSessionClientImpl sessionClient; 17 | 18 | private final Intent intent; 19 | 20 | public static final String PARAM_CLICK_TIME = "clickTime"; 21 | 22 | public static final String PARAM_LOAD_URL_TIME = "loadUrlTime"; 23 | 24 | public SonicJavaScriptInterface(SonicSessionClientImpl sessionClient, Intent intent) { 25 | this.sessionClient = sessionClient; 26 | this.intent = intent; 27 | } 28 | 29 | @JavascriptInterface 30 | public void getDiffData() { 31 | // the callback function of demo page is hardcode as 'getDiffDataCallback' 32 | getDiffData2("getDiffDataCallback"); 33 | } 34 | 35 | @JavascriptInterface 36 | public void getDiffData2(final String jsCallbackFunc) { 37 | if (null != sessionClient) { 38 | sessionClient.getDiffData(new SonicDiffDataCallback() { 39 | @Override 40 | public void callback(final String resultData) { 41 | Runnable callbackRunnable = new Runnable() { 42 | @Override 43 | public void run() { 44 | String jsCode = "javascript:" + jsCallbackFunc + "('"+ toJsString(resultData) + "')"; 45 | sessionClient.getWebView().loadUrl(jsCode); 46 | } 47 | }; 48 | if (Looper.getMainLooper() == Looper.myLooper()) { 49 | callbackRunnable.run(); 50 | } else { 51 | new Handler(Looper.getMainLooper()).post(callbackRunnable); 52 | } 53 | } 54 | }); 55 | } 56 | } 57 | 58 | @JavascriptInterface 59 | public String getPerformance() { 60 | long clickTime = intent.getLongExtra(PARAM_CLICK_TIME, -1); 61 | long loadUrlTime = intent.getLongExtra(PARAM_LOAD_URL_TIME, -1); 62 | try { 63 | JSONObject result = new JSONObject(); 64 | result.put(PARAM_CLICK_TIME, clickTime); 65 | result.put(PARAM_LOAD_URL_TIME, loadUrlTime); 66 | return result.toString(); 67 | } catch (Exception e) { 68 | 69 | } 70 | 71 | return ""; 72 | } 73 | 74 | /* 75 | * * From RFC 4627, "All Unicode characters may be placed within the quotation marks except 76 | * for the characters that must be escaped: quotation mark, 77 | * reverse solidus, and the control characters (U+0000 through U+001F)." 78 | */ 79 | private static String toJsString(String value) { 80 | if (value == null) { 81 | return "null"; 82 | } 83 | StringBuilder out = new StringBuilder(1024); 84 | for (int i = 0, length = value.length(); i < length; i++) { 85 | char c = value.charAt(i); 86 | 87 | 88 | switch (c) { 89 | case '"': 90 | case '\\': 91 | case '/': 92 | out.append('\\').append(c); 93 | break; 94 | 95 | case '\t': 96 | out.append("\\t"); 97 | break; 98 | 99 | case '\b': 100 | out.append("\\b"); 101 | break; 102 | 103 | case '\n': 104 | out.append("\\n"); 105 | break; 106 | 107 | case '\r': 108 | out.append("\\r"); 109 | break; 110 | 111 | case '\f': 112 | out.append("\\f"); 113 | break; 114 | 115 | default: 116 | if (c <= 0x1F) { 117 | out.append(String.format("\\u%04x", (int) c)); 118 | } else { 119 | out.append(c); 120 | } 121 | break; 122 | } 123 | 124 | } 125 | return out.toString(); 126 | } 127 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/webview/SonicRuntimeImpl.java: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.webview; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.text.TextUtils; 6 | import android.util.Log; 7 | import android.webkit.CookieManager; 8 | import android.webkit.WebResourceResponse; 9 | import com.tencent.sonic.sdk.SonicRuntime; 10 | import com.tencent.sonic.sdk.SonicSessionClient; 11 | import me.luowl.wan.BuildConfig; 12 | import me.luowl.wan.WanApplication; 13 | 14 | import java.io.File; 15 | import java.io.InputStream; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * the sonic host application must implement SonicRuntime to do right things. 21 | */ 22 | 23 | public class SonicRuntimeImpl extends SonicRuntime { 24 | 25 | public SonicRuntimeImpl(Context context) { 26 | super(context); 27 | } 28 | 29 | /** 30 | * 获取用户UA信息 31 | * 32 | * @return 33 | */ 34 | @Override 35 | public String getUserAgent() { 36 | return "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Mobile Safari/537.36"; 37 | } 38 | 39 | /** 40 | * 获取用户ID信息 41 | * 42 | * @return 43 | */ 44 | @Override 45 | public String getCurrentUserAccount() { 46 | return "sonic-demo-master"; 47 | } 48 | 49 | @Override 50 | public String getCookie(String url) { 51 | CookieManager cookieManager = CookieManager.getInstance(); 52 | return cookieManager.getCookie(url); 53 | } 54 | 55 | @Override 56 | public void log(String tag, int level, String message) { 57 | switch (level) { 58 | case Log.ERROR: 59 | Log.e(tag, message); 60 | break; 61 | case Log.INFO: 62 | Log.i(tag, message); 63 | break; 64 | default: 65 | Log.d(tag, message); 66 | } 67 | } 68 | 69 | @Override 70 | public Object createWebResourceResponse(String mimeType, String encoding, InputStream data, Map headers) { 71 | WebResourceResponse resourceResponse = new WebResourceResponse(mimeType, encoding, data); 72 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 73 | resourceResponse.setResponseHeaders(headers); 74 | } 75 | return resourceResponse; 76 | } 77 | 78 | @Override 79 | public void showToast(CharSequence text, int duration) { 80 | 81 | } 82 | 83 | @Override 84 | public void notifyError(SonicSessionClient client, String url, int errorCode) { 85 | 86 | } 87 | 88 | @Override 89 | public boolean isSonicUrl(String url) { 90 | return true; 91 | } 92 | 93 | @Override 94 | public boolean setCookie(String url, List cookies) { 95 | if (!TextUtils.isEmpty(url) && cookies != null && cookies.size() > 0) { 96 | CookieManager cookieManager = CookieManager.getInstance(); 97 | for (String cookie : cookies) { 98 | cookieManager.setCookie(url, cookie); 99 | } 100 | return true; 101 | } 102 | return false; 103 | } 104 | 105 | @Override 106 | public boolean isNetworkValid() { 107 | return true; 108 | } 109 | 110 | @Override 111 | public void postTaskToThread(Runnable task, long delayMillis) { 112 | Thread thread = new Thread(task, "SonicThread"); 113 | thread.start(); 114 | } 115 | 116 | @Override 117 | public File getSonicCacheDir() { 118 | if (BuildConfig.DEBUG) { 119 | String path = WanApplication.context.getExternalCacheDir().getAbsolutePath() + File.separator + "sonic/"; 120 | File file = new File(path.trim()); 121 | if (!file.exists()) { 122 | file.mkdir(); 123 | } 124 | return file; 125 | } 126 | return super.getSonicCacheDir(); 127 | } 128 | 129 | @Override 130 | public String getHostDirectAddress(String url) { 131 | return null; 132 | } 133 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/webview/SonicSessionClientImpl.java: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.webview; 2 | 3 | import android.os.Bundle; 4 | import android.webkit.WebView; 5 | import com.tencent.sonic.sdk.SonicSessionClient; 6 | 7 | import java.util.HashMap; 8 | 9 | /** 10 | * a implement of SonicSessionClient which need to connect webview and content data. 11 | */ 12 | 13 | public class SonicSessionClientImpl extends SonicSessionClient { 14 | 15 | private WebView webView; 16 | 17 | public void bindWebView(WebView webView) { 18 | this.webView = webView; 19 | } 20 | 21 | public WebView getWebView() { 22 | return webView; 23 | } 24 | 25 | @Override 26 | public void loadUrl(String url, Bundle extraData) { 27 | webView.loadUrl(url); 28 | } 29 | 30 | @Override 31 | public void loadDataWithBaseUrl(String baseUrl, String data, String mimeType, String encoding, String historyUrl) { 32 | webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); 33 | } 34 | 35 | 36 | @Override 37 | public void loadDataWithBaseUrlAndHeader(String baseUrl, String data, String mimeType, String encoding, String historyUrl, HashMap headers) { 38 | loadDataWithBaseUrl(baseUrl, data, mimeType, encoding, historyUrl); 39 | } 40 | 41 | public void destroy() { 42 | if (null != webView) { 43 | webView.destroy(); 44 | webView = null; 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/webview/WebViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.webview 2 | 3 | import me.luowl.wan.base.BaseViewModel 4 | 5 | /* 6 | * 7 | * Created by luowl 8 | * Date: 2019/8/8 9 | * Desc: 10 | */ 11 | 12 | class WebViewModel : BaseViewModel() { 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/wxarticle/ArticleListViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.wxarticle 2 | 3 | import me.luowl.wan.base.ArticlePageListViewModel 4 | import me.luowl.wan.data.WanRepository 5 | import me.luowl.wan.data.model.BaseResp 6 | import me.luowl.wan.data.model.PageData 7 | 8 | /* 9 | * 10 | * Created by luowl 11 | * Date: 2019/7/25 12 | * Desc: 13 | */ 14 | 15 | class ArticleListViewModel(repository: WanRepository) : ArticlePageListViewModel(repository) { 16 | 17 | var chapterId: Long = 0L 18 | var keyword: String? = null 19 | 20 | override suspend fun request(): BaseResp { 21 | return if (hasKeyWord()) { 22 | repository.getWXArticleListByKey(chapterId, pageIndex, keyword!!) 23 | } else { 24 | repository.getWXArticleList(chapterId, pageIndex) 25 | } 26 | } 27 | 28 | private fun hasKeyWord(): Boolean { 29 | return keyword != null && keyword!!.isNotEmpty() 30 | } 31 | 32 | fun reset() { 33 | pageIndex = 0 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/wxarticle/WXArticleChaptersFragment.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.wxarticle 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.databinding.ViewDataBinding 6 | import androidx.lifecycle.Observer 7 | import androidx.recyclerview.widget.GridLayoutManager 8 | import kotlinx.android.synthetic.main.fragment_home.* 9 | import me.luowl.wan.BR 10 | import me.luowl.wan.R 11 | import me.luowl.wan.base.BaseFragment 12 | import me.luowl.wan.base.adapter.RVAdapter 13 | import me.luowl.wan.data.model.Architecture 14 | import me.luowl.wan.databinding.FragmentWxArticleChaptersBinding 15 | import me.luowl.wan.util.GlobalUtil 16 | import me.luowl.wan.util.logDebug 17 | import me.luowl.wan.widget.SimpleDividerItemDecoration 18 | 19 | /* 20 | * 21 | * Created by luowl 22 | * Date: 2019/7/24 23 | * Desc: 24 | */ 25 | 26 | class WXArticleChaptersFragment : BaseFragment() { 27 | 28 | private lateinit var adapter: RVAdapter 29 | 30 | override fun getViewModelClass(): Class = WXArticleChaptersViewModel::class.java 31 | 32 | override fun getLayoutId() = R.layout.fragment_wx_article_chapters 33 | 34 | override fun initVariableId() = BR.viewModel 35 | 36 | override fun setupViews(savedInstanceState: Bundle?) { 37 | super.setupViews(savedInstanceState) 38 | recycler_view.layoutManager = GridLayoutManager(context, 2) 39 | recycler_view.addItemDecoration( 40 | SimpleDividerItemDecoration( 41 | context, 42 | 20f, 43 | GlobalUtil.getColor(R.color.white), 44 | 20f, 15f, 15f 45 | ) 46 | ) 47 | adapter = object : RVAdapter(mutableListOf(), R.layout.recycler_item_wx_chapter, BR.data) { 48 | override fun addListener(binding: ViewDataBinding, itemData: Architecture, position: Int) { 49 | binding.root.setOnClickListener { 50 | context?.let { 51 | ArticleListActivity.startActivity(it, itemData.id, itemData.name) 52 | } 53 | } 54 | } 55 | } 56 | recycler_view.adapter = adapter 57 | } 58 | 59 | override fun initViewObservable() { 60 | super.initViewObservable() 61 | viewModel.dataList.observe(this, Observer { 62 | logDebug("dataChange") 63 | adapter.setData(viewModel.dataList.value) 64 | }) 65 | } 66 | 67 | override fun startLoadData() { 68 | super.startLoadData() 69 | viewModel.dataList.value?.run { 70 | if (isEmpty()) { 71 | viewModel.getData() 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/ui/wxarticle/WXArticleChaptersViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.ui.wxarticle 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import me.luowl.wan.base.BaseViewModel 5 | import me.luowl.wan.data.WanRepository 6 | import me.luowl.wan.data.model.Architecture 7 | 8 | /* 9 | * 10 | * Created by luowl 11 | * Date: 2019/7/24 12 | * Desc: 13 | */ 14 | 15 | class WXArticleChaptersViewModel constructor(private val repository: WanRepository) : BaseViewModel() { 16 | 17 | private val _items = MutableLiveData>().apply { value = mutableListOf() } 18 | val dataList: MutableLiveData> = _items 19 | 20 | fun getData() { 21 | launch { 22 | stateModel.startLoading() 23 | val resp = repository.getWXArticleChapters() 24 | checkResponseCode(resp) 25 | val data = resp.data ?: listOf() 26 | if (data.isNotEmpty()) { 27 | dataList.value = ArrayList(data) 28 | stateModel.loadDataFinish() 29 | } else { 30 | stateModel.showEmpty() 31 | } 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/util/BindingAdapters.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.util 2 | 3 | import android.widget.ImageView 4 | import androidx.databinding.BindingAdapter 5 | import androidx.databinding.InverseBindingAdapter 6 | import androidx.databinding.InverseBindingListener 7 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout 8 | import com.bumptech.glide.Glide 9 | import me.luowl.wan.R 10 | 11 | /* 12 | * 13 | * Created by luowl 14 | * Date: 2019/7/25 15 | * Desc: 16 | */ 17 | 18 | object BindingAdapters { 19 | @BindingAdapter(value = ["app:imageUrl"], requireAll = true) 20 | @JvmStatic 21 | fun loadImage(view: ImageView, url: String) { 22 | Glide.with(view.context).load(url).into(view) 23 | } 24 | 25 | @BindingAdapter(value = ["app:collect"], requireAll = true) 26 | @JvmStatic 27 | fun setCollectionImageSrc(view: ImageView, collect: Boolean) { 28 | view.setImageResource(if (collect) R.mipmap.ic_collect else R.mipmap.ic_uncollect) 29 | } 30 | 31 | //是否刷新中 32 | @JvmStatic 33 | @BindingAdapter("refreshing") 34 | fun setRefreshing(swipeRefreshLayout: SwipeRefreshLayout, refreshing: Boolean) { 35 | if (swipeRefreshLayout.isRefreshing != refreshing) 36 | swipeRefreshLayout.isRefreshing = refreshing 37 | } 38 | 39 | @JvmStatic 40 | @InverseBindingAdapter(attribute = "refreshing", event = "onRefresh") 41 | fun isRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean { 42 | logDebug("refreshing:$swipeRefreshLayout.isRefreshing") 43 | return swipeRefreshLayout.isRefreshing 44 | } 45 | 46 | @JvmStatic 47 | @BindingAdapter("onRefresh", requireAll = false) 48 | fun setOnRefreshListener(swipeRefreshLayout: SwipeRefreshLayout, bindingListener: InverseBindingListener?) { 49 | if (bindingListener != null) 50 | swipeRefreshLayout.setOnRefreshListener { 51 | bindingListener.onChange() 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/util/BindingConverters.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.util 2 | 3 | import android.graphics.drawable.Drawable 4 | import android.graphics.drawable.GradientDrawable 5 | import androidx.databinding.BindingConversion 6 | import me.luowl.wan.R 7 | import me.luowl.wan.WanApplication 8 | 9 | /* 10 | * 11 | * Created by luowl 12 | * Date: 2019/7/25 13 | * Desc: 14 | */ 15 | 16 | object BindingConverters { 17 | @BindingConversion 18 | @JvmStatic 19 | fun convertStringToDrawable(value: Long): Drawable { 20 | val colors = WanApplication.context.resources.obtainTypedArray(R.array.chapter_colors) 21 | val color = colors.getColor((value % colors.length()).toInt(), 0) 22 | val gradientDrawable = GradientDrawable() 23 | gradientDrawable.setColor(color) 24 | gradientDrawable.cornerRadius = 30f 25 | return gradientDrawable 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/util/GlobalUtil.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.util 2 | 3 | import android.widget.Toast 4 | import androidx.core.content.ContextCompat 5 | import me.luowl.wan.WanApplication 6 | 7 | 8 | /** 9 | * 应用程序全局的通用工具类,功能比较单一,经常被复用的功能,应该封装到此工具类当中,从而给全局代码提供方面的操作。 10 | * 11 | */ 12 | object GlobalUtil { 13 | 14 | private var TAG = "GlobalUtil" 15 | 16 | private val toast: Toast by lazy { 17 | Toast.makeText(WanApplication.context,null,Toast.LENGTH_SHORT) 18 | } 19 | 20 | fun showToastShort(msg: String) { 21 | toast.setText(msg) 22 | toast.duration = Toast.LENGTH_SHORT 23 | toast.show() 24 | } 25 | 26 | /** 27 | * 获取当前应用程序的包名。 28 | * 29 | * @return 当前应用程序的包名。 30 | */ 31 | val appPackage: String 32 | get() = WanApplication.context.packageName 33 | 34 | /** 35 | * 获取资源文件中定义的字符串。 36 | * 37 | * @param resId 38 | * 字符串资源id 39 | * @return 字符串资源id对应的字符串内容。 40 | */ 41 | fun getString(resId: Int): String { 42 | return WanApplication.context.resources.getString(resId) 43 | } 44 | 45 | /** 46 | * 获取指定资源名的资源id。 47 | * 48 | * @param name 49 | * 资源名 50 | * @param type 51 | * 资源类型 52 | * @return 指定资源名的资源id。 53 | */ 54 | fun getResourceId(name: String, type: String): Int { 55 | return WanApplication.context.resources.getIdentifier(name, type, appPackage) 56 | } 57 | 58 | fun getColor(resId: Int): Int { 59 | return ContextCompat.getColor(WanApplication.context, resId) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/util/Log.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.util 2 | 3 | import android.os.Build 4 | import timber.log.Timber 5 | 6 | /** 7 | * 日志操作的扩展工具类。 8 | * 9 | */ 10 | 11 | fun Any.logVerbose(msg: String?) { 12 | Timber.v(msg) 13 | } 14 | 15 | fun Any.logDebug(msg: String?) { 16 | Timber.v(msg) 17 | } 18 | 19 | fun Any.logInfo(msg: String?) { 20 | Timber.i(msg) 21 | } 22 | 23 | fun Any.logWarn(msg: String?, tr: Throwable? = null) { 24 | Timber.w(msg) 25 | } 26 | 27 | fun Any.logError(msg: String?, tr: Throwable) { 28 | Timber.e(tr, msg) 29 | } 30 | 31 | fun logVerbose(tag: String, msg: String?) { 32 | Timber.tag(tag).v(tag, msg) 33 | } 34 | 35 | fun logDebug(tag: String, msg: String?) { 36 | Timber.tag(tag).d(msg) 37 | } 38 | 39 | fun logInfo(tag: String, msg: String?) { 40 | Timber.tag(tag).i(msg) 41 | } 42 | 43 | fun logWarn(tag: String, msg: String?, tr: Throwable? = null) { 44 | Timber.tag(tag).w(tag, msg) 45 | } 46 | 47 | fun logError(tag: String, msg: String?, tr: Throwable) { 48 | Timber.tag(tag).e(tag, msg) 49 | } 50 | 51 | fun printlnDeviceInfo(){ 52 | val head = "************* Log Head ****************" + 53 | "\nDevice Manufacturer 制造商 : " + Build.MANUFACTURER + 54 | "\nDevice Model 型号 : " + Build.MODEL + 55 | "\nIdentifier_Brand 品牌 : " + Build.BRAND + 56 | "\nIdentifier_Device 设备名 : " + Build.DEVICE + 57 | "\nAndroid Version : " + Build.VERSION.RELEASE + 58 | "\nAndroid SDK : " + Build.VERSION.SDK_INT + 59 | "\n************* Log Head ****************\n\n" 60 | logDebug("Device",head) 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/util/Preference.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import me.luowl.wan.WanApplication 7 | import java.io.* 8 | import kotlin.reflect.KProperty 9 | 10 | /** 11 | * kotlin委托属性+SharedPreference实例 12 | */ 13 | class Preference(val name: String, private val default: T) { 14 | 15 | companion object { 16 | private val file_name = "wan_android_file" 17 | 18 | private val prefs: SharedPreferences by lazy { 19 | WanApplication.context.getSharedPreferences(file_name, Context.MODE_PRIVATE) 20 | } 21 | 22 | /** 23 | * 删除全部数据 24 | */ 25 | fun clearPreference() { 26 | prefs.edit().clear().apply() 27 | } 28 | 29 | /** 30 | * 根据key删除存储数据 31 | */ 32 | fun clearPreference(key: String) { 33 | prefs.edit().remove(key).apply() 34 | } 35 | 36 | /** 37 | * 查询某个key是否已经存在 38 | * 39 | * @param key 40 | * @return 41 | */ 42 | fun contains(key: String): Boolean { 43 | return prefs.contains(key) 44 | } 45 | 46 | /** 47 | * 返回所有的键值对 48 | * 49 | * @param context 50 | * @return 51 | */ 52 | fun getAll(): Map { 53 | return prefs.all 54 | } 55 | } 56 | 57 | operator fun getValue(thisRef: Any?, property: KProperty<*>): T { 58 | return getSharedPreferences(name, default) 59 | } 60 | 61 | operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { 62 | putSharedPreferences(name, value) 63 | } 64 | 65 | @SuppressLint("CommitPrefEdits") 66 | private fun putSharedPreferences(name: String, value: T) = with(prefs.edit()) { 67 | when (value) { 68 | is Long -> putLong(name, value) 69 | is String -> putString(name, value) 70 | is Int -> putInt(name, value) 71 | is Boolean -> putBoolean(name, value) 72 | is Float -> putFloat(name, value) 73 | else -> putString(name, serialize(value)) 74 | }.apply() 75 | } 76 | 77 | @Suppress("UNCHECKED_CAST") 78 | private fun getSharedPreferences(name: String, default: T): T = with(prefs) { 79 | val res: Any = when (default) { 80 | is Long -> getLong(name, default) 81 | is String -> getString(name, default)!! 82 | is Int -> getInt(name, default) 83 | is Boolean -> getBoolean(name, default) 84 | is Float -> getFloat(name, default) 85 | else -> deSerialization(getString(name, serialize(default))!!) 86 | } 87 | return res as T 88 | } 89 | 90 | /** 91 | * 序列化对象 92 | * @param person 93 | * * 94 | * @return 95 | * * 96 | * @throws IOException 97 | */ 98 | @Throws(IOException::class) 99 | private fun serialize(obj: A): String { 100 | val byteArrayOutputStream = ByteArrayOutputStream() 101 | val objectOutputStream = ObjectOutputStream( 102 | byteArrayOutputStream 103 | ) 104 | objectOutputStream.writeObject(obj) 105 | var serStr = byteArrayOutputStream.toString("ISO-8859-1") 106 | serStr = java.net.URLEncoder.encode(serStr, "UTF-8") 107 | objectOutputStream.close() 108 | byteArrayOutputStream.close() 109 | return serStr 110 | } 111 | 112 | /** 113 | * 反序列化对象 114 | * @param str 115 | * * 116 | * @return 117 | * * 118 | * @throws IOException 119 | * * 120 | * @throws ClassNotFoundException 121 | */ 122 | @Suppress("UNCHECKED_CAST") 123 | @Throws(IOException::class, ClassNotFoundException::class) 124 | private fun deSerialization(str: String): A { 125 | val redStr = java.net.URLDecoder.decode(str, "UTF-8") 126 | val byteArrayInputStream = ByteArrayInputStream( 127 | redStr.toByteArray(charset("ISO-8859-1")) 128 | ) 129 | val objectInputStream = ObjectInputStream( 130 | byteArrayInputStream 131 | ) 132 | val obj = objectInputStream.readObject() as A 133 | objectInputStream.close() 134 | byteArrayInputStream.close() 135 | return obj 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/util/WanGlideModel.java: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.util; 2 | /* 3 | * 4 | * Created by luowl 5 | * Date: 2019/8/26 6 | * Desc: 7 | */ 8 | 9 | import android.content.Context; 10 | 11 | import androidx.annotation.NonNull; 12 | 13 | import com.bumptech.glide.Glide; 14 | import com.bumptech.glide.Registry; 15 | import com.bumptech.glide.annotation.GlideModule; 16 | import com.bumptech.glide.module.AppGlideModule; 17 | 18 | @GlideModule(glideName = "WanGlide") 19 | public class WanGlideModel extends AppGlideModule { 20 | @Override 21 | public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { 22 | super.registerComponents(context, glide, registry); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/me/luowl/wan/widget/SimpleDividerItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package me.luowl.wan.widget 2 | 3 | import android.content.Context 4 | import androidx.recyclerview.widget.GridLayoutManager 5 | import androidx.recyclerview.widget.LinearLayoutManager 6 | import androidx.recyclerview.widget.RecyclerView 7 | import me.luowl.wan.R 8 | import me.luowl.wan.base.adapter.RVAdapter 9 | import me.luowl.wan.util.GlobalUtil 10 | 11 | /** 12 | * 13 | * Created by luowl@iPanel.cn 14 | * Date: 2019/7/23 15 | * Desc: 16 | */ 17 | class SimpleDividerItemDecoration( 18 | context: Context?, private val dividerWidth: Float = 0.5f, 19 | val color: Int = GlobalUtil.getColor(R.color.app_divider), 20 | private val firstTop: Float = 0f, private val firstLeft: Float = 0f, private val lastRight: Float = 0f 21 | ) : RVDividerItemDecoration(context) { 22 | override fun getDivider(parent: RecyclerView, itemPosition: Int): RVDivider { 23 | when (parent.adapter?.getItemViewType(itemPosition)) { 24 | RVAdapter.TYPE_LOAD_MORE -> return RVDividerBuilder().create() 25 | } 26 | if (parent.layoutManager is GridLayoutManager) { 27 | val spanCount = getSpanCount(parent) 28 | val rvDividerBuilder = RVDividerBuilder() 29 | if (itemPosition < spanCount) { 30 | rvDividerBuilder.setTopSideLine(true, color, firstTop, 0f, 0f) 31 | } 32 | return when (itemPosition % spanCount) { 33 | 0 -> 34 | //每一行第一个填充右边 35 | rvDividerBuilder 36 | .setLeftSideLine(true, color, firstLeft, 0f, 0f) 37 | .setRightSideLine(true, color, dividerWidth / 2, 0f, 0f) 38 | .setBottomSideLine(true, color, dividerWidth, 0f, 0f) 39 | spanCount - 1 -> 40 | //第行最后一个填充左边 41 | rvDividerBuilder 42 | .setLeftSideLine(true, color, dividerWidth / 2, 0f, 0f) 43 | .setRightSideLine(true, color, lastRight, 0f, 0f) 44 | .setBottomSideLine(true, color, dividerWidth, 0f, 0f) 45 | else -> rvDividerBuilder 46 | .setLeftSideLine(true, color, dividerWidth / 2, 0f, 0f) 47 | .setRightSideLine(true, color, dividerWidth / 2, 0f, 0f) 48 | .setBottomSideLine(true, color, dividerWidth, 0f, 0f) 49 | }.create() 50 | } 51 | if (parent.layoutManager is LinearLayoutManager) { 52 | val rvDividerBuilder = RVDividerBuilder() 53 | .setBottomSideLine(true, color, dividerWidth, 0f, 0f) 54 | if (itemPosition == 0) 55 | rvDividerBuilder.setTopSideLine(true, color, firstTop, 0f, 0f) 56 | rvDividerBuilder.setLeftSideLine(true, color, firstLeft, 0f, 0f) 57 | rvDividerBuilder.setRightSideLine(true, color, lastRight, 0f, 0f) 58 | return rvDividerBuilder.create() 59 | } 60 | return RVDividerBuilder().create() 61 | } 62 | 63 | private fun getSpanCount(parent: RecyclerView): Int { 64 | val layoutManager = parent.layoutManager 65 | return (layoutManager as? GridLayoutManager)?.spanCount ?: 1 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/progress_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selector_list_item_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_knowledge_sub_child.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_login_edit_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_top_article.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shaper_fliter.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_architecture_category.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 21 | 22 | 37 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_coin_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 15 | 16 | 24 | 25 | 28 | 29 | 33 | 34 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_collection.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 17 | 18 | 26 | 27 | 30 | 31 | 35 | 36 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_container.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 15 | 16 | 24 | 25 | 38 | 39 | 43 | 44 | 57 | 58 |