├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── debug.keystore ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── top │ │ └── wangchenyan │ │ └── wancompose │ │ ├── Navigation.kt │ │ ├── WanApplication.kt │ │ ├── api │ │ └── Api.kt │ │ ├── auth │ │ ├── AuthManager.kt │ │ └── UserData.kt │ │ ├── storage │ │ ├── AppPreferences.kt │ │ ├── HistoryPreferences.kt │ │ └── PreferencesDataStore.kt │ │ ├── theme │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ │ ├── ui │ │ ├── Main.kt │ │ ├── MainActivity.kt │ │ ├── home │ │ │ ├── Home.kt │ │ │ ├── model │ │ │ │ ├── ArticleData.kt │ │ │ │ └── HomeData.kt │ │ │ └── viewmodel │ │ │ │ └── HomeViewModel.kt │ │ ├── mine │ │ │ ├── CollectList.kt │ │ │ ├── Login.kt │ │ │ ├── Mine.kt │ │ │ ├── Register.kt │ │ │ └── viewmodel │ │ │ │ ├── CollectViewModel.kt │ │ │ │ ├── LoginViewModel.kt │ │ │ │ ├── MineViewModel.kt │ │ │ │ └── RegisterViewModel.kt │ │ ├── search │ │ │ ├── Search.kt │ │ │ ├── SearchResult.kt │ │ │ ├── model │ │ │ │ └── SearchData.kt │ │ │ └── viewmodel │ │ │ │ ├── SearchResultViewModel.kt │ │ │ │ └── SearchViewModel.kt │ │ ├── square │ │ │ ├── Square.kt │ │ │ └── viewmodel │ │ │ │ └── SquareViewModel.kt │ │ ├── web │ │ │ ├── Web.kt │ │ │ └── viewmodel │ │ │ │ └── WebViewModel.kt │ │ └── wechat │ │ │ ├── WeChat.kt │ │ │ ├── model │ │ │ └── WeChatData.kt │ │ │ └── viewmodel │ │ │ ├── WeChatTabViewModel.kt │ │ │ └── WeChatViewModel.kt │ │ └── widget │ │ ├── Banner.kt │ │ ├── BottomTab.kt │ │ ├── PageLoading.kt │ │ └── TitleLayout.kt │ └── res │ ├── drawable-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── drawable-hdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable-xhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── drawable-xxhdpi │ ├── android_q.webp │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── drawable-xxxhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── drawable │ ├── ic_arrow_right.xml │ ├── ic_back.xml │ ├── ic_close.xml │ ├── ic_launcher_background.xml │ ├── ic_like.xml │ ├── ic_like_fill.xml │ ├── ic_search.xml │ ├── ic_share.xml │ ├── ic_tab_discover.xml │ ├── ic_tab_discover_fill.xml │ ├── ic_tab_home.xml │ ├── ic_tab_home_fill.xml │ ├── ic_tab_my.xml │ ├── ic_tab_my_fill.xml │ ├── ic_tab_wechat.xml │ └── ic_tab_wechat_fill.xml │ ├── values-night │ └── themes.xml │ └── values │ ├── colors.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── themes.xml ├── art ├── qrcode.png ├── screenshot01.jpg └── screenshot02.jpg ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wan-compose 2 | 3 | Jetpack Compose 版本的「玩 Android」 4 | 5 | ## 环境 6 | - AndroidStudio: Arctic Fox | 2020.3.1 7 | 8 | ## 功能 9 | - 登录、注册 10 | - 等级、排名、积分、收藏 11 | - 首页、搜索 12 | - 广场 13 | - 公众号 14 | 15 | ## 下载体验 16 | ![](https://raw.githubusercontent.com/wangchenyan/wan-compose/master/art/qrcode.png) 17 | 18 | https://github.com/wangchenyan/wan-compose/releases 19 | 20 | ## 截图 21 | ![](https://raw.githubusercontent.com/wangchenyan/wan-compose/master/art/screenshot01.jpg) 22 | 23 | ![](https://raw.githubusercontent.com/wangchenyan/wan-compose/master/art/screenshot02.jpg) 24 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | android { 7 | compileSdk = 34 8 | namespace = "top.wangchenyan.wancompose" 9 | 10 | defaultConfig { 11 | applicationId = "top.wangchenyan.wancompose" 12 | minSdk = 21 13 | targetSdk = 33 14 | versionCode = 1 15 | versionName = "1.0" 16 | 17 | vectorDrawables { 18 | useSupportLibrary = true 19 | } 20 | } 21 | 22 | signingConfigs { 23 | getByName("debug") { 24 | storeFile = file("debug.keystore") 25 | } 26 | } 27 | 28 | buildTypes { 29 | release { 30 | isMinifyEnabled = true 31 | proguardFiles( 32 | getDefaultProguardFile("proguard-android-optimize.txt"), 33 | "proguard-rules.pro" 34 | ) 35 | signingConfig = signingConfigs.getByName("debug") 36 | } 37 | debug { 38 | signingConfig = signingConfigs.getByName("debug") 39 | } 40 | } 41 | compileOptions { 42 | sourceCompatibility = JavaVersion.VERSION_1_8 43 | targetCompatibility = JavaVersion.VERSION_1_8 44 | } 45 | kotlinOptions { 46 | jvmTarget = "1.8" 47 | } 48 | buildFeatures { 49 | compose = true 50 | } 51 | composeOptions { 52 | kotlinCompilerExtensionVersion = "1.4.3" 53 | } 54 | } 55 | 56 | dependencies { 57 | implementation("androidx.core:core-ktx:1.9.0") 58 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") 59 | implementation("androidx.activity:activity-compose:1.8.1") 60 | implementation(platform("androidx.compose:compose-bom:2023.03.00")) 61 | implementation("androidx.compose.ui:ui") 62 | implementation("androidx.compose.ui:ui-graphics") 63 | implementation("androidx.compose.ui:ui-tooling-preview") 64 | implementation("androidx.compose.material3:material3") 65 | implementation("androidx.datastore:datastore-preferences:1.0.0") 66 | implementation("androidx.navigation:navigation-compose:2.7.5") 67 | implementation("com.google.accompanist:accompanist-pager:0.10.0") 68 | implementation("com.google.accompanist:accompanist-coil:0.10.0") 69 | implementation("com.google.accompanist:accompanist-swiperefresh:0.10.0") 70 | implementation("com.google.accompanist:accompanist-flowlayout:0.10.0") 71 | implementation("com.github.wangchenyan.mock-http:mock-http:1.7") 72 | implementation("com.github.franmontiel:PersistentCookieJar:v1.0.1") 73 | implementation("com.github.wangchenyan:android-common:1.0.0-beta12") 74 | implementation("com.github.jenly1314.UltraSwipeRefresh:refresh:1.0.0") 75 | } -------------------------------------------------------------------------------- /app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/debug.keystore -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/Navigation.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.navigation.compose.NavHost 5 | import androidx.navigation.compose.composable 6 | import androidx.navigation.compose.rememberNavController 7 | import com.google.accompanist.pager.ExperimentalPagerApi 8 | import top.wangchenyan.wancompose.ui.Main 9 | import top.wangchenyan.wancompose.ui.mine.CollectList 10 | import top.wangchenyan.wancompose.ui.mine.Login 11 | import top.wangchenyan.wancompose.ui.mine.Register 12 | import top.wangchenyan.wancompose.ui.search.Search 13 | import top.wangchenyan.wancompose.ui.search.SearchResult 14 | import top.wangchenyan.wancompose.ui.web.Web 15 | 16 | @ExperimentalPagerApi 17 | @Composable 18 | fun ComposeNavigation() { 19 | val navController = rememberNavController() 20 | NavHost(navController = navController, startDestination = "main") { 21 | composable("main") { Main(navController) } 22 | composable("web?url={url}") { backStackEntry -> 23 | val url = backStackEntry.arguments?.getString("url") ?: "" 24 | Web(navController, url) 25 | } 26 | composable("login") { Login(navController) } 27 | composable("register") { Register(navController) } 28 | composable("collect") { CollectList(navController) } 29 | composable("search") { Search(navController) } 30 | composable("search_result?keyword={keyword}") { backStackEntry -> 31 | val keyword = backStackEntry.arguments?.getString("keyword") ?: "" 32 | SearchResult(navController, keyword) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/WanApplication.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | import me.wcy.mockhttp.MockHttp 6 | import me.wcy.mockhttp.MockHttpOptions 7 | import top.wangchenyan.common.CommonApp 8 | import top.wangchenyan.wancompose.auth.AuthManager 9 | 10 | /** 11 | * Created by wcy on 2021/4/1. 12 | */ 13 | class WanApplication : Application() { 14 | 15 | override fun onCreate() { 16 | super.onCreate() 17 | CommonApp.init { 18 | test = true 19 | apiConfig({}) { 20 | codeJsonNames = listOf("errorCode") 21 | msgJsonNames = listOf("errorMsg") 22 | dataJsonNames = listOf("data") 23 | successCode = 0 24 | } 25 | } 26 | AuthManager.init() 27 | val options = MockHttpOptions.Builder() 28 | .setMockServerPort(3000) 29 | .setMockSleepTime(500) 30 | .setLogEnable(true) 31 | .setLogLevel(Log.ERROR) 32 | .build() 33 | MockHttp.get().setMockHttpOptions(options) 34 | MockHttp.get().start(this) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/api/Api.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.api 2 | 3 | import com.franmontiel.persistentcookiejar.PersistentCookieJar 4 | import com.franmontiel.persistentcookiejar.cache.SetCookieCache 5 | import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor 6 | import me.wcy.mockhttp.MockHttpInterceptor 7 | import okhttp3.OkHttpClient 8 | import retrofit2.Retrofit 9 | import retrofit2.http.* 10 | import top.wangchenyan.common.CommonApp 11 | import top.wangchenyan.common.net.NetResult 12 | import top.wangchenyan.common.net.gson.GsonConverterFactory 13 | import top.wangchenyan.common.utils.GsonUtils 14 | import top.wangchenyan.wancompose.auth.User 15 | import top.wangchenyan.wancompose.ui.home.model.Article 16 | import top.wangchenyan.wancompose.ui.home.model.ArticleList 17 | import top.wangchenyan.wancompose.ui.home.model.HomeBannerData 18 | import top.wangchenyan.wancompose.ui.search.model.HotKey 19 | import top.wangchenyan.wancompose.ui.wechat.model.WeChatAuthor 20 | import java.util.concurrent.TimeUnit 21 | 22 | /** 23 | * Created by wcy on 2021/4/1. 24 | */ 25 | interface Api { 26 | @GET("banner/json") 27 | suspend fun getHomeBanner(): NetResult> 28 | 29 | @GET("article/list/{page}/json") 30 | suspend fun getHomeArticleList(@Path("page") page: Int = 0): NetResult 31 | 32 | @GET("article/top/json") 33 | suspend fun getStickyArticle(): NetResult> 34 | 35 | @GET("user_article/list/{page}/json") 36 | suspend fun getSquareArticleList(@Path("page") page: Int = 0): NetResult 37 | 38 | @GET("wxarticle/chapters/json") 39 | suspend fun getWeChatAuthorList(): NetResult> 40 | 41 | @GET("wxarticle/list/{id}/{page}/json") 42 | suspend fun getWeChatArticleList( 43 | @Path("id") id: Long, 44 | @Path("page") page: Int = 0 45 | ): NetResult 46 | 47 | @FormUrlEncoded 48 | @POST("user/login") 49 | suspend fun login( 50 | @Field("username") username: String, 51 | @Field("password") password: String 52 | ): NetResult 53 | 54 | @FormUrlEncoded 55 | @POST("user/register") 56 | suspend fun register( 57 | @Field("username") username: String, 58 | @Field("password") password: String, 59 | @Field("repassword") repassword: String 60 | ): NetResult 61 | 62 | @GET("user/logout/json") 63 | suspend fun logout(): NetResult 64 | 65 | @GET("lg/coin/userinfo/json") 66 | suspend fun getUserCoin(): NetResult 67 | 68 | @GET("lg/collect/list/{page}/json") 69 | suspend fun getCollectArticleList(@Path("page") page: Int = 0): NetResult 70 | 71 | @POST("lg/collect/{id}/json") 72 | suspend fun collect(@Path("id") id: Long): NetResult 73 | 74 | @POST("lg/uncollect_originId/{id}/json") 75 | suspend fun uncollect(@Path("id") id: Long): NetResult 76 | 77 | @GET("hotkey/json") 78 | suspend fun searchHotKey(): NetResult> 79 | 80 | @POST("article/query/{page}/json") 81 | suspend fun search( 82 | @Path("page") page: Int, 83 | @Query("k") keyword: String, 84 | ): NetResult 85 | 86 | companion object { 87 | private const val BASE_URL = "https://www.wanandroid.com/" 88 | 89 | private val okHttpClient: OkHttpClient by lazy { 90 | OkHttpClient.Builder() 91 | .connectTimeout(15, TimeUnit.SECONDS) 92 | .readTimeout(15, TimeUnit.SECONDS) 93 | .writeTimeout(15, TimeUnit.SECONDS) 94 | .addInterceptor(MockHttpInterceptor()) 95 | .cookieJar( 96 | PersistentCookieJar( 97 | SetCookieCache(), 98 | SharedPrefsCookiePersistor(CommonApp.app) 99 | ) 100 | ) 101 | .build() 102 | } 103 | 104 | private val api by lazy { 105 | val retrofit = Retrofit.Builder() 106 | .baseUrl(BASE_URL) 107 | .addConverterFactory( 108 | GsonConverterFactory.create(GsonUtils.gson, true) 109 | ) 110 | .client(okHttpClient) 111 | .build() 112 | retrofit.create(Api::class.java) 113 | } 114 | 115 | fun get(): Api { 116 | return api 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/auth/AuthManager.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.auth 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.launch 7 | import kotlinx.coroutines.withContext 8 | import top.wangchenyan.common.CommonApp 9 | import top.wangchenyan.common.net.apiCall 10 | import top.wangchenyan.wancompose.api.Api 11 | import top.wangchenyan.wancompose.storage.AppPreferences 12 | 13 | object AuthManager { 14 | private var userInternal = MutableLiveData() 15 | val user: LiveData = userInternal 16 | 17 | fun init() { 18 | CommonApp.appScope.launch { 19 | withContext(Dispatchers.Main) { 20 | userInternal.value = AppPreferences.getUser() 21 | if (isLogin()) { 22 | updateUserCoin() 23 | } 24 | } 25 | } 26 | } 27 | 28 | private suspend fun updateUserCoin() { 29 | val userRes = apiCall { Api.get().getUserCoin() } 30 | if (userRes.isSuccessWithData()) { 31 | val user = userRes.data!! 32 | val rawUser = userInternal.value!!.apply { 33 | coinCount = user.coinCount 34 | level = user.level 35 | rank = user.rank 36 | } 37 | AppPreferences.setUser(rawUser) 38 | userInternal.postValue(rawUser) 39 | } else if (userRes.code == -1001) { 40 | onLogout() 41 | } 42 | } 43 | 44 | fun isLogin(): Boolean { 45 | return userInternal.value != null 46 | } 47 | 48 | fun onLogin(user: User) { 49 | CommonApp.appScope.launch { 50 | userInternal.postValue(user) 51 | AppPreferences.setUser(user) 52 | updateUserCoin() 53 | } 54 | } 55 | 56 | fun onLogout() { 57 | CommonApp.appScope.launch { 58 | userInternal.postValue(null) 59 | AppPreferences.setUser(null) 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/auth/UserData.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.auth 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class User( 6 | @SerializedName("id") val id: Long = 0, 7 | @SerializedName("username") val username: String = "", 8 | @SerializedName("nickname") val nickname: String = "", 9 | @SerializedName("chapterTops") val chapterTops: List = listOf(), 10 | @SerializedName("coinCount") var coinCount: Int = 0, 11 | @SerializedName("collectIds") val collectIds: List = listOf(), 12 | @SerializedName("email") val email: String = "", 13 | @SerializedName("icon") val icon: String = "", 14 | @SerializedName("level") var level: String = "", 15 | @SerializedName("rank") var rank: String = "" 16 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/storage/AppPreferences.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.storage 2 | 3 | import androidx.datastore.preferences.core.stringPreferencesKey 4 | import top.wangchenyan.common.CommonApp 5 | import top.wangchenyan.wancompose.auth.User 6 | 7 | object AppPreferences { 8 | private val KEY_USER = stringPreferencesKey("user") 9 | 10 | private val dataStore by lazy { 11 | PreferencesDataStore(CommonApp.app, CommonApp.app.packageName + ".app") 12 | } 13 | 14 | suspend fun getUser(): User? { 15 | return dataStore.getModel(KEY_USER) 16 | } 17 | 18 | suspend fun setUser(user: User?) { 19 | dataStore.putModel(KEY_USER, user) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/storage/HistoryPreferences.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.storage 2 | 3 | import androidx.datastore.preferences.core.stringPreferencesKey 4 | import top.wangchenyan.common.CommonApp 5 | 6 | object HistoryPreferences { 7 | private val KEY_HISTORY = stringPreferencesKey("history") 8 | 9 | private val dataStore by lazy { 10 | PreferencesDataStore( 11 | CommonApp.app, 12 | CommonApp.app.packageName + ".history" 13 | ) 14 | } 15 | 16 | suspend fun getHistory(): List { 17 | return dataStore.getList(KEY_HISTORY) ?: listOf() 18 | } 19 | 20 | suspend fun addHistory(item: String) { 21 | if (item.isEmpty()) { 22 | return 23 | } 24 | val list = getHistory().toMutableList() 25 | if (list.contains(item)) { 26 | return 27 | } 28 | list.add(0, item) 29 | dataStore.putList(KEY_HISTORY, list) 30 | } 31 | 32 | suspend fun removeHistory(item: String) { 33 | if (item.isEmpty()) { 34 | return 35 | } 36 | val list = getHistory().toMutableList() 37 | if (!list.contains(item)) { 38 | return 39 | } 40 | list.remove(item) 41 | dataStore.putList(KEY_HISTORY, list) 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/storage/PreferencesDataStore.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.storage 2 | 3 | import android.content.Context 4 | import androidx.datastore.preferences.core.* 5 | import androidx.datastore.preferences.preferencesDataStoreFile 6 | import kotlinx.coroutines.flow.catch 7 | import kotlinx.coroutines.flow.first 8 | import kotlinx.coroutines.flow.map 9 | import top.wangchenyan.common.utils.GsonUtils 10 | import java.io.IOException 11 | 12 | /** 13 | * Created by wcy on 2021/2/25. 14 | */ 15 | class PreferencesDataStore(context: Context, name: String) { 16 | private val ds by lazy { 17 | PreferenceDataStoreFactory.create { 18 | context.applicationContext.preferencesDataStoreFile(name) 19 | } 20 | } 21 | 22 | suspend fun remove(key: Preferences.Key) = 23 | ds.edit { it.remove(key) } 24 | 25 | suspend fun clear() = 26 | ds.edit { it.clear() } 27 | 28 | suspend inline fun get(key: String): T = get(preferencesKey(key)) 29 | 30 | suspend inline fun get(key: Preferences.Key): T = 31 | get( 32 | key, when (T::class.java) { 33 | Boolean::class.javaObjectType -> false as T 34 | Int::class.javaObjectType -> 0 as T 35 | Long::class.javaObjectType -> 0L as T 36 | Float::class.javaObjectType -> 0F as T 37 | Double::class.javaObjectType -> 0.0 as T 38 | String::class.javaObjectType -> "" as T 39 | else -> throw IllegalArgumentException("不支持的类型") 40 | } 41 | ) 42 | 43 | suspend fun get(key: Preferences.Key, defValue: T): T = 44 | ds.data 45 | .catch { exception -> 46 | // dataStore.data throws an IOException when an error is encountered when reading data 47 | if (exception is IOException) { 48 | emit(emptyPreferences()) 49 | } else { 50 | throw exception 51 | } 52 | }.map { preferences -> 53 | preferences[key] ?: defValue 54 | }.first() 55 | 56 | suspend inline fun put(key: String, value: T) = put(preferencesKey(key), value) 57 | 58 | suspend fun put(key: Preferences.Key, value: T) = ds.edit { it[key] = value } 59 | 60 | suspend inline fun getModel(key: Preferences.Key): T? { 61 | val json = get(key) 62 | if (json.isNotEmpty()) { 63 | return GsonUtils.fromJson(json, T::class.java) 64 | } 65 | return null 66 | } 67 | 68 | suspend inline fun putModel(key: Preferences.Key, t: T) { 69 | if (t == null) { 70 | remove(key) 71 | } else { 72 | put(key, GsonUtils.toJson(t) ?: "") 73 | } 74 | } 75 | 76 | suspend inline fun getList(key: Preferences.Key): List? { 77 | val json = get(key) 78 | if (json.isNotEmpty()) { 79 | return GsonUtils.fromJsonList(json) 80 | } 81 | return null 82 | } 83 | 84 | suspend inline fun putList(key: Preferences.Key, list: List) = 85 | putModel(key, list) 86 | 87 | @Suppress("UNCHECKED_CAST") 88 | inline fun preferencesKey(key: String): Preferences.Key = 89 | when (T::class.java) { 90 | Boolean::class.javaObjectType -> booleanPreferencesKey(key) as Preferences.Key 91 | Int::class.javaObjectType -> intPreferencesKey(key) as Preferences.Key 92 | Long::class.javaObjectType -> longPreferencesKey(key) as Preferences.Key 93 | Float::class.javaObjectType -> floatPreferencesKey(key) as Preferences.Key 94 | Double::class.javaObjectType -> doublePreferencesKey(key) as Preferences.Key 95 | String::class.javaObjectType -> stringPreferencesKey(key) as Preferences.Key 96 | else -> throw IllegalArgumentException("不支持的类型") 97 | } 98 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFBB86FC) 6 | val Purple500 = Color(0xFF6200EE) 7 | val Purple700 = Color(0xFF3700B3) 8 | val Teal200 = Color(0xFF03DAC5) 9 | 10 | object Colors { 11 | val white = Color.White 12 | val green = Color(0xFF4CAF50) 13 | val red = Color(0xFFF44336) 14 | val blue = Color(0xFF2196F3) 15 | val main = green 16 | val background = Color(0xFFF4F4F4) 17 | val unselect = Color(0xFF424242) 18 | val titleBar = Color(0xFFFAFAFA) 19 | val bottomBar = Color(0xFFFAFAFA) 20 | const val text_h1_int = 0xFF212121 21 | val text_h1 = Color(text_h1_int) 22 | val text_h2 = Color(0xFF757575) 23 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.darkColorScheme 6 | import androidx.compose.material3.lightColorScheme 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColorScheme( 10 | primary = Colors.main, 11 | secondary = Colors.main, 12 | tertiary = Colors.main 13 | ) 14 | 15 | private val LightColorPalette = lightColorScheme( 16 | primary = Colors.main, 17 | secondary = Colors.main, 18 | tertiary = Colors.main 19 | 20 | /* Other default colors to override 21 | background = Color(0xFFFFFBFE), 22 | surface = Color(0xFFFFFBFE), 23 | onPrimary = Color.White, 24 | onSecondary = Color.White, 25 | onTertiary = Color.White, 26 | onBackground = Color(0xFF1C1B1F), 27 | onSurface = Color(0xFF1C1B1F), 28 | */ 29 | ) 30 | 31 | @Composable 32 | fun WanandroidTheme( 33 | darkTheme: Boolean = isSystemInDarkTheme(), 34 | content: @Composable() () -> Unit 35 | ) { 36 | val colors = if (darkTheme) { 37 | DarkColorPalette 38 | } else { 39 | LightColorPalette 40 | } 41 | 42 | MaterialTheme( 43 | colorScheme = colors, 44 | typography = Typography, 45 | content = content 46 | ) 47 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/Main.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.rememberCoroutineScope 7 | import androidx.compose.ui.Modifier 8 | import androidx.navigation.NavHostController 9 | import com.google.accompanist.pager.ExperimentalPagerApi 10 | import com.google.accompanist.pager.HorizontalPager 11 | import com.google.accompanist.pager.rememberPagerState 12 | import kotlinx.coroutines.launch 13 | import top.wangchenyan.wancompose.ui.home.Home 14 | import top.wangchenyan.wancompose.ui.mine.Mine 15 | import top.wangchenyan.wancompose.ui.square.Square 16 | import top.wangchenyan.wancompose.ui.wechat.WeChat 17 | import top.wangchenyan.wancompose.widget.BottomTab 18 | 19 | @ExperimentalPagerApi 20 | @Composable 21 | fun Main(navController: NavHostController) { 22 | Column(Modifier.fillMaxSize()) { 23 | val scope = rememberCoroutineScope() 24 | val pagerState = rememberPagerState(pageCount = 4, initialOffscreenLimit = 3) 25 | HorizontalPager(pagerState, Modifier.weight(1f), dragEnabled = false) { page -> 26 | when (page) { 27 | 0 -> { 28 | Home(navController) 29 | } 30 | 31 | 1 -> { 32 | Square(navController) 33 | } 34 | 35 | 2 -> { 36 | WeChat(navController) 37 | } 38 | 39 | 3 -> { 40 | Mine(navController) 41 | } 42 | } 43 | } 44 | BottomTab(pagerState.currentPage) { 45 | scope.launch { 46 | pagerState.scrollToPage(it) 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import com.google.accompanist.pager.ExperimentalPagerApi 7 | import top.wangchenyan.wancompose.ComposeNavigation 8 | import top.wangchenyan.wancompose.theme.WanandroidTheme 9 | 10 | class MainActivity : ComponentActivity() { 11 | @ExperimentalPagerApi 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContent { 15 | WanandroidTheme { 16 | ComposeNavigation() 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/home/Home.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.home 2 | 3 | import android.text.TextUtils 4 | import android.util.TypedValue 5 | import android.widget.TextView 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.border 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.Row 12 | import androidx.compose.foundation.layout.Spacer 13 | import androidx.compose.foundation.layout.fillMaxSize 14 | import androidx.compose.foundation.layout.fillMaxWidth 15 | import androidx.compose.foundation.layout.height 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.size 18 | import androidx.compose.foundation.layout.width 19 | import androidx.compose.foundation.lazy.LazyColumn 20 | import androidx.compose.foundation.lazy.itemsIndexed 21 | import androidx.compose.foundation.shape.RoundedCornerShape 22 | import androidx.compose.material3.Divider 23 | import androidx.compose.material3.Icon 24 | import androidx.compose.material3.Text 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.res.painterResource 29 | import androidx.compose.ui.unit.dp 30 | import androidx.compose.ui.unit.sp 31 | import androidx.compose.ui.viewinterop.AndroidView 32 | import androidx.lifecycle.viewmodel.compose.viewModel 33 | import androidx.navigation.NavHostController 34 | import com.google.accompanist.pager.ExperimentalPagerApi 35 | import com.king.ultraswiperefresh.NestedScrollMode 36 | import com.king.ultraswiperefresh.UltraSwipeRefresh 37 | import top.wangchenyan.wancompose.R 38 | import top.wangchenyan.wancompose.auth.AuthManager 39 | import top.wangchenyan.wancompose.theme.Colors 40 | import top.wangchenyan.wancompose.ui.home.model.Article 41 | import top.wangchenyan.wancompose.ui.home.model.HomeBannerData 42 | import top.wangchenyan.wancompose.ui.home.viewmodel.HomeViewModel 43 | import top.wangchenyan.wancompose.widget.Banner 44 | import top.wangchenyan.wancompose.widget.BannerData 45 | import top.wangchenyan.wancompose.widget.PageLoading 46 | import top.wangchenyan.wancompose.widget.TitleLayout 47 | 48 | /** 49 | * Created by wcy on 2021/3/31. 50 | */ 51 | 52 | @ExperimentalPagerApi 53 | @Composable 54 | fun Home(navController: NavHostController) { 55 | val viewModel: HomeViewModel = viewModel() 56 | Column( 57 | Modifier 58 | .fillMaxSize() 59 | .background(Colors.background) 60 | ) { 61 | TitleLayout( 62 | title = "首页", 63 | menuIcon = R.drawable.ic_search, 64 | onMenuClick = { 65 | navController.navigate("search") 66 | } 67 | ) 68 | PageLoading( 69 | loadState = viewModel.pageState, 70 | showLoading = viewModel.showLoading, 71 | onReload = { viewModel.firstLoad() }) { 72 | UltraSwipeRefresh( 73 | state = viewModel.refreshState, 74 | onRefresh = { viewModel.onRefresh() }, 75 | onLoadMore = { viewModel.onLoad() }, 76 | headerScrollMode = NestedScrollMode.FixedContent, 77 | footerScrollMode = NestedScrollMode.FixedContent, 78 | ) { 79 | LazyColumn( 80 | Modifier 81 | .fillMaxSize() 82 | .background(Colors.white) 83 | ) { 84 | itemsIndexed(viewModel.list) { index, item -> 85 | if (item is List<*>) { 86 | BannerItem(navController, item as List) 87 | } else if (item is Article) { 88 | ArticleItem(navController, item) { 89 | viewModel.collect(item) 90 | } 91 | Divider(Modifier.padding(16.dp, 0.dp), thickness = 0.5.dp) 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | @ExperimentalPagerApi 101 | @Composable 102 | fun BannerItem(navController: NavHostController, list: List) { 103 | val dataList = list.map { 104 | BannerData(it.title, it.imagePath, it.url) 105 | } 106 | Banner( 107 | navController = navController, 108 | modifier = Modifier 109 | .fillMaxWidth() 110 | .height(220.dp), 111 | dataList = dataList 112 | ) 113 | } 114 | 115 | @Composable 116 | fun ArticleItem( 117 | navController: NavHostController, 118 | article: Article, 119 | onCollectClick: () -> Unit = {} 120 | ) { 121 | Box(modifier = Modifier 122 | .fillMaxWidth() 123 | .clickable { 124 | navController.navigate("web?url=${article.link}") 125 | }) { 126 | Column( 127 | Modifier.padding(16.dp, 10.dp) 128 | ) { 129 | Row(Modifier.fillMaxWidth()) { 130 | article.tags.forEach { 131 | Text( 132 | it.name, 133 | Modifier 134 | .align(Alignment.CenterVertically) 135 | .border(0.5.dp, it.getColor(), RoundedCornerShape(3.dp)) 136 | .padding(2.dp, 1.dp), 137 | it.getColor(), 138 | 10.sp 139 | ) 140 | Spacer( 141 | modifier = Modifier 142 | .align(Alignment.CenterVertically) 143 | .width(8.dp) 144 | .height(0.dp) 145 | ) 146 | } 147 | Text( 148 | article.getAuthor(), 149 | Modifier 150 | .weight(1f) 151 | .align(Alignment.CenterVertically), 152 | Colors.text_h2, 153 | 12.sp 154 | ) 155 | Spacer( 156 | modifier = Modifier 157 | .align(Alignment.CenterVertically) 158 | .width(10.dp) 159 | .height(0.dp) 160 | ) 161 | Text( 162 | article.niceDate, 163 | Modifier 164 | .align(Alignment.CenterVertically), 165 | Colors.text_h2, 166 | 12.sp 167 | ) 168 | } 169 | Spacer( 170 | modifier = Modifier 171 | .fillMaxWidth() 172 | .height(4.dp) 173 | ) 174 | AndroidView( 175 | modifier = Modifier 176 | .fillMaxWidth(), 177 | factory = { context -> 178 | TextView(context).apply { 179 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f) 180 | setTextColor(Colors.text_h1_int.toInt()) 181 | maxLines = 2 182 | ellipsize = TextUtils.TruncateAt.END 183 | } 184 | }, 185 | update = { 186 | it.text = article.getSpannableTitle() 187 | } 188 | ) 189 | Row( 190 | modifier = Modifier 191 | .fillMaxWidth() 192 | .padding(top = 5.dp) 193 | ) { 194 | val chapter = StringBuilder(article.superChapterName) 195 | if (article.superChapterName.isNotEmpty() && article.chapterName.isNotEmpty()) { 196 | chapter.append(" / ") 197 | } 198 | chapter.append(article.chapterName) 199 | Text( 200 | chapter.toString(), 201 | Modifier 202 | .weight(1f) 203 | .align(Alignment.CenterVertically), 204 | Colors.text_h2, 205 | 12.sp, 206 | ) 207 | val iconRes = if (article.collect) R.drawable.ic_like_fill else R.drawable.ic_like 208 | val tint = if (article.collect) Colors.red else Colors.text_h2 209 | Icon( 210 | painter = painterResource(id = iconRes), 211 | contentDescription = "收藏", 212 | modifier = Modifier 213 | .size(24.dp) 214 | .align(Alignment.CenterVertically) 215 | .clickable { 216 | if (!AuthManager.isLogin()) { 217 | navController.navigate("login") 218 | } else { 219 | onCollectClick.invoke() 220 | } 221 | }, 222 | tint = tint 223 | ) 224 | } 225 | } 226 | } 227 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/home/model/ArticleData.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.home.model 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.google.gson.annotations.SerializedName 5 | import top.wangchenyan.wancompose.theme.Colors 6 | 7 | data class ArticleList( 8 | @SerializedName("curPage") val curPage: Int = 0, 9 | @SerializedName("datas") val datas: List
= listOf(), 10 | @SerializedName("offset") val offset: Int = 0, 11 | @SerializedName("over") val over: Boolean = false, 12 | @SerializedName("pageCount") val pageCount: Int = 0, 13 | @SerializedName("size") val size: Int = 0, 14 | @SerializedName("total") val total: Int = 0 15 | ) 16 | 17 | data class Article( 18 | @SerializedName("id") val id: Long = 0, 19 | @SerializedName("apkLink") val apkLink: String = "", 20 | @SerializedName("audit") val audit: Int = 0, 21 | @SerializedName("author") private val author: String = "", 22 | @SerializedName("canEdit") val canEdit: Boolean = false, 23 | @SerializedName("chapterId") val chapterId: Int = 0, 24 | @SerializedName("chapterName") val chapterName: String = "", 25 | @SerializedName("collect") var collect: Boolean = false, 26 | @SerializedName("courseId") val courseId: Int = 0, 27 | @SerializedName("desc") val desc: String = "", 28 | @SerializedName("descMd") val descMd: String = "", 29 | @SerializedName("envelopePic") val envelopePic: String = "", 30 | @SerializedName("fresh") val fresh: Boolean = false, 31 | @SerializedName("host") val host: String = "", 32 | @SerializedName("link") val link: String = "", 33 | @SerializedName("niceDate") val niceDate: String = "", 34 | @SerializedName("niceShareDate") val niceShareDate: String = "", 35 | @SerializedName("origin") val origin: String = "", 36 | @SerializedName("prefix") val prefix: String = "", 37 | @SerializedName("projectLink") val projectLink: String = "", 38 | @SerializedName("publishTime") val publishTime: Long = 0, 39 | @SerializedName("realSuperChapterId") val realSuperChapterId: Int = 0, 40 | @SerializedName("selfVisible") val selfVisible: Int = 0, 41 | @SerializedName("shareDate") val shareDate: Long = 0, 42 | @SerializedName("shareUser") val shareUser: String = "", 43 | @SerializedName("superChapterId") val superChapterId: Int = 0, 44 | @SerializedName("superChapterName") val superChapterName: String = "", 45 | @SerializedName("tags") val tags: MutableList = mutableListOf(), 46 | @SerializedName("title") val title: String = "", 47 | @SerializedName("type") val type: Int = 0, 48 | @SerializedName("userId") val userId: Int = 0, 49 | @SerializedName("visible") val visible: Int = 0, 50 | @SerializedName("zan") val zan: Int = 0, 51 | @SerializedName("originId") val originId: Long = 0 52 | ) { 53 | private var spannableTitle: CharSequence? = null 54 | 55 | fun getAuthor(): String { 56 | return if (author.isNotEmpty()) author else shareUser 57 | } 58 | 59 | fun getSpannableTitle(): CharSequence { 60 | return spannableTitle ?: title 61 | } 62 | 63 | fun setSpannableTitle(title: CharSequence?) { 64 | spannableTitle = title 65 | } 66 | } 67 | 68 | data class ArticleTag( 69 | @SerializedName("name") val name: String = "", 70 | @SerializedName("url") val url: String = "" 71 | ) { 72 | fun getColor(): Color { 73 | return when (name) { 74 | "置顶" -> Colors.red 75 | "本站发布" -> Color(0xFF2196F3) 76 | "问答" -> Color(0xFF00BCD4) 77 | "公众号" -> Color(0xFF4CAF50) 78 | "项目" -> Color(0xFF009688) 79 | else -> Colors.main 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/home/model/HomeData.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.home.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | /** 7 | * Created by wcy on 2021/4/1. 8 | */ 9 | 10 | data class HomeBannerData( 11 | @SerializedName("id") val id: Long = 0, 12 | @SerializedName("desc") val desc: String = "", 13 | @SerializedName("imagePath") val imagePath: String = "", 14 | @SerializedName("isVisible") val isVisible: Int = 0, 15 | @SerializedName("order") val order: Int = 0, 16 | @SerializedName("title") val title: String = "", 17 | @SerializedName("type") val type: Int = 0, 18 | @SerializedName("url") val url: String = "" 19 | ) 20 | -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/home/viewmodel/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.home.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.king.ultraswiperefresh.UltraSwipeRefreshState 9 | import kotlinx.coroutines.async 10 | import kotlinx.coroutines.launch 11 | import top.wangchenyan.common.ext.toast 12 | import top.wangchenyan.common.net.apiCall 13 | import top.wangchenyan.wancompose.api.Api 14 | import top.wangchenyan.wancompose.ui.home.model.Article 15 | import top.wangchenyan.wancompose.ui.home.model.ArticleTag 16 | import top.wangchenyan.wancompose.ui.mine.viewmodel.CollectViewModel 17 | import top.wangchenyan.wancompose.widget.LoadState 18 | 19 | /** 20 | * Created by wcy on 2021/4/1. 21 | */ 22 | class HomeViewModel : ViewModel() { 23 | var pageState by mutableStateOf(LoadState.LOADING) 24 | var showLoading by mutableStateOf(false) 25 | var list by mutableStateOf(listOf()) 26 | var refreshState by mutableStateOf( 27 | UltraSwipeRefreshState( 28 | isRefreshing = false, 29 | isLoading = false 30 | ) 31 | ) 32 | private var page = 0 33 | 34 | init { 35 | firstLoad() 36 | } 37 | 38 | fun firstLoad() { 39 | viewModelScope.launch { 40 | page = 0 41 | pageState = LoadState.LOADING 42 | val bannerDeffer = async { apiCall { Api.get().getHomeBanner() } } 43 | val stickDeffer = async { apiCall { Api.get().getStickyArticle() } } 44 | val articleDeffer = async { apiCall { Api.get().getHomeArticleList() } } 45 | val bannerRes = bannerDeffer.await() 46 | val stickyRes = stickDeffer.await() 47 | val articleRes = articleDeffer.await() 48 | if (bannerRes.isSuccessWithData() && articleRes.isSuccessWithData() && stickyRes.isSuccessWithData()) { 49 | pageState = LoadState.SUCCESS 50 | list = mutableListOf().apply { 51 | add(bannerRes.data!!) 52 | addAll(stickyRes.data!!.onEach { 53 | it.tags.add(0, ArticleTag("置顶")) 54 | }) 55 | addAll(articleRes.data!!.datas) 56 | } 57 | } else { 58 | pageState = LoadState.FAIL 59 | } 60 | } 61 | } 62 | 63 | fun onRefresh() { 64 | viewModelScope.launch { 65 | page = 0 66 | refreshState.isRefreshing = true 67 | val bannerDeffer = async { apiCall { Api.get().getHomeBanner() } } 68 | val stickDeffer = async { apiCall { Api.get().getStickyArticle() } } 69 | val articleDeffer = async { apiCall { Api.get().getHomeArticleList() } } 70 | val bannerRes = bannerDeffer.await() 71 | val stickyRes = stickDeffer.await() 72 | val articleRes = articleDeffer.await() 73 | if (bannerRes.isSuccessWithData() && articleRes.isSuccessWithData() && stickyRes.isSuccessWithData()) { 74 | list = mutableListOf().apply { 75 | add(bannerRes.data!!) 76 | addAll(stickyRes.data!!.onEach { 77 | it.tags.add(0, ArticleTag("置顶")) 78 | }) 79 | addAll(articleRes.data!!.datas) 80 | } 81 | refreshState.isRefreshing = false 82 | } else { 83 | refreshState.isRefreshing = false 84 | toast("加载失败") 85 | } 86 | } 87 | } 88 | 89 | fun onLoad() { 90 | viewModelScope.launch { 91 | refreshState.isLoading = true 92 | val articleList = apiCall { Api.get().getHomeArticleList(page + 1) } 93 | if (articleList.isSuccessWithData()) { 94 | page++ 95 | list = list.toMutableList().apply { 96 | addAll(articleList.data!!.datas) 97 | } 98 | refreshState.isLoading = false 99 | } else { 100 | refreshState.isLoading = false 101 | toast("加载失败") 102 | } 103 | } 104 | } 105 | 106 | fun collect(article: Article) { 107 | viewModelScope.launch { 108 | showLoading = true 109 | CollectViewModel.collect(article) 110 | showLoading = false 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/mine/CollectList.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.mine 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.foundation.lazy.itemsIndexed 9 | import androidx.compose.material3.Divider 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import androidx.lifecycle.viewmodel.compose.viewModel 14 | import androidx.navigation.NavHostController 15 | import com.king.ultraswiperefresh.NestedScrollMode 16 | import com.king.ultraswiperefresh.UltraSwipeRefresh 17 | import top.wangchenyan.wancompose.theme.Colors 18 | import top.wangchenyan.wancompose.ui.home.ArticleItem 19 | import top.wangchenyan.wancompose.ui.mine.viewmodel.CollectViewModel 20 | import top.wangchenyan.wancompose.widget.PageLoading 21 | import top.wangchenyan.wancompose.widget.TitleLayout 22 | 23 | @Composable 24 | fun CollectList(navController: NavHostController) { 25 | val viewModel: CollectViewModel = viewModel() 26 | Column( 27 | Modifier 28 | .fillMaxSize() 29 | .background(Colors.background) 30 | ) { 31 | TitleLayout(title = "我的收藏", onBack = { navController.popBackStack() }) 32 | PageLoading( 33 | loadState = viewModel.pageState, 34 | onReload = { viewModel.firstLoad() }) { 35 | UltraSwipeRefresh( 36 | state = viewModel.refreshState, 37 | onRefresh = { viewModel.onRefresh() }, 38 | onLoadMore = { viewModel.onLoad() }, 39 | headerScrollMode = NestedScrollMode.FixedContent, 40 | footerScrollMode = NestedScrollMode.FixedContent, 41 | ) { 42 | LazyColumn( 43 | Modifier 44 | .fillMaxSize() 45 | .background(Colors.white) 46 | ) { 47 | itemsIndexed(viewModel.list) { index, item -> 48 | ArticleItem(navController, item) { 49 | viewModel.uncollect(item) 50 | } 51 | Divider(Modifier.padding(16.dp, 0.dp), thickness = 0.5.dp) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/mine/Login.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.mine 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.foundation.text.KeyboardOptions 8 | import androidx.compose.material.* 9 | import androidx.compose.material3.Button 10 | import androidx.compose.material3.Divider 11 | import androidx.compose.material3.ExperimentalMaterial3Api 12 | import androidx.compose.material3.OutlinedTextField 13 | import androidx.compose.material3.Text 14 | import androidx.compose.material3.TextFieldDefaults 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.text.input.KeyboardType 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import androidx.lifecycle.viewmodel.compose.viewModel 23 | import androidx.navigation.NavHostController 24 | import top.wangchenyan.wancompose.theme.Colors 25 | import top.wangchenyan.wancompose.ui.mine.viewmodel.LoginViewModel 26 | import top.wangchenyan.wancompose.widget.PageLoading 27 | import top.wangchenyan.wancompose.widget.TitleLayout 28 | 29 | @OptIn(ExperimentalMaterial3Api::class) 30 | @Composable 31 | fun Login(navController: NavHostController) { 32 | val viewModel: LoginViewModel = viewModel() 33 | PageLoading( 34 | modifier = Modifier.background(Colors.background), 35 | showLoading = viewModel.showLoading 36 | ) { 37 | Column(Modifier.fillMaxSize()) { 38 | TitleLayout(title = "登录", onBack = { 39 | navController.popBackStack() 40 | }) 41 | Column(Modifier.fillMaxSize()) { 42 | Spacer( 43 | modifier = Modifier 44 | .height(120.dp) 45 | ) 46 | OutlinedTextField( 47 | value = viewModel.username, 48 | onValueChange = { value -> 49 | viewModel.username = value 50 | }, 51 | modifier = Modifier.fillMaxWidth(), 52 | label = { 53 | Text(text = "请输入用户名") 54 | }, 55 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), 56 | singleLine = true, 57 | colors = TextFieldDefaults.outlinedTextFieldColors( 58 | focusedBorderColor = Color.Transparent, 59 | unfocusedBorderColor = Color.Transparent 60 | ) 61 | ) 62 | Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) 63 | Spacer(modifier = Modifier.height(10.dp)) 64 | OutlinedTextField( 65 | value = viewModel.password, 66 | onValueChange = { value -> 67 | viewModel.password = value 68 | }, 69 | modifier = Modifier.fillMaxWidth(), 70 | label = { 71 | Text(text = "请输入密码") 72 | }, 73 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), 74 | singleLine = true, 75 | colors = TextFieldDefaults.outlinedTextFieldColors( 76 | focusedBorderColor = Color.Transparent, 77 | unfocusedBorderColor = Color.Transparent 78 | ) 79 | ) 80 | Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) 81 | Spacer(modifier = Modifier.height(50.dp)) 82 | Button( 83 | onClick = { viewModel.login(navController) }, 84 | modifier = Modifier 85 | .fillMaxWidth() 86 | .height(44.dp) 87 | .padding(horizontal = 16.dp), 88 | shape = RoundedCornerShape(percent = 50), 89 | ) { 90 | Text(text = "登录", fontSize = 15.sp) 91 | } 92 | Spacer(modifier = Modifier.height(20.dp)) 93 | Text( 94 | text = "没有账号?去注册", 95 | modifier = Modifier 96 | .align(Alignment.End) 97 | .padding(horizontal = 16.dp) 98 | .clickable { 99 | navController.navigate("register") 100 | }, 101 | color = Colors.text_h2, 102 | fontSize = 15.sp 103 | ) 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/mine/Mine.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.mine 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.foundation.layout.width 16 | import androidx.compose.foundation.shape.RoundedCornerShape 17 | import androidx.compose.material3.AlertDialog 18 | import androidx.compose.material3.Button 19 | import androidx.compose.material3.ButtonDefaults 20 | import androidx.compose.material3.Divider 21 | import androidx.compose.material3.Icon 22 | import androidx.compose.material3.Text 23 | import androidx.compose.material3.TextButton 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.res.painterResource 29 | import androidx.compose.ui.text.TextStyle 30 | import androidx.compose.ui.unit.dp 31 | import androidx.compose.ui.unit.sp 32 | import androidx.lifecycle.viewmodel.compose.viewModel 33 | import androidx.navigation.NavHostController 34 | import top.wangchenyan.wancompose.R 35 | import top.wangchenyan.wancompose.theme.Colors 36 | import top.wangchenyan.wancompose.ui.mine.viewmodel.MineViewModel 37 | import top.wangchenyan.wancompose.widget.PageLoading 38 | 39 | /** 40 | * Created by wcy on 2021/3/31. 41 | */ 42 | 43 | @Composable 44 | fun Mine(navController: NavHostController) { 45 | val viewModel: MineViewModel = viewModel() 46 | PageLoading( 47 | showLoading = viewModel.showLoading, 48 | modifier = Modifier.background(Colors.background) 49 | ) { 50 | Column( 51 | modifier = Modifier.fillMaxSize(), 52 | horizontalAlignment = Alignment.CenterHorizontally 53 | ) { 54 | Spacer(modifier = Modifier.height(30.dp)) 55 | Image( 56 | painter = painterResource(id = R.drawable.android_q), 57 | contentDescription = "", 58 | modifier = Modifier 59 | .width(100.dp) 60 | .height(100.dp) 61 | .clickable { 62 | if (viewModel.user == null) { 63 | navController.navigate("login") 64 | } 65 | } 66 | ) 67 | Text( 68 | text = viewModel.user?.nickname ?: "未登录", 69 | modifier = Modifier.clickable { 70 | if (viewModel.user == null) { 71 | navController.navigate("login") 72 | } 73 | }, 74 | color = Colors.text_h1 75 | ) 76 | Spacer(modifier = Modifier.height(15.dp)) 77 | Row { 78 | Box( 79 | modifier = Modifier.background(Colors.green) 80 | ) { 81 | Text( 82 | text = "LV" + (viewModel.user?.level ?: ""), 83 | modifier = Modifier 84 | .padding(horizontal = 4.dp, vertical = 2.dp), 85 | style = TextStyle(color = Color.White) 86 | ) 87 | } 88 | Spacer(modifier = Modifier.width(5.dp)) 89 | Box( 90 | modifier = Modifier.background(Colors.blue) 91 | ) { 92 | Text( 93 | text = "排名" + (viewModel.user?.rank ?: ""), 94 | modifier = Modifier 95 | .padding(horizontal = 4.dp, vertical = 2.dp), 96 | style = TextStyle(color = Color.White) 97 | ) 98 | } 99 | } 100 | Spacer(modifier = Modifier.height(50.dp)) 101 | Row( 102 | modifier = Modifier 103 | .fillMaxWidth() 104 | .height(50.dp) 105 | .background(Color.White) 106 | .clickable { 107 | if (viewModel.user == null) { 108 | navController.navigate("login") 109 | } 110 | } 111 | ) { 112 | Spacer(modifier = Modifier.width(16.dp)) 113 | Text( 114 | text = "我的积分", 115 | modifier = Modifier 116 | .align(Alignment.CenterVertically) 117 | .weight(1f), 118 | color = Colors.text_h1 119 | ) 120 | if (viewModel.user != null) { 121 | Text( 122 | text = viewModel.user!!.coinCount.toString(), 123 | modifier = Modifier 124 | .align(Alignment.CenterVertically), 125 | color = Colors.text_h2 126 | ) 127 | } 128 | Spacer(modifier = Modifier.width(16.dp)) 129 | } 130 | Divider(color = Colors.background, thickness = 0.5.dp) 131 | Row( 132 | modifier = Modifier 133 | .fillMaxWidth() 134 | .height(50.dp) 135 | .background(Color.White) 136 | .clickable { 137 | if (viewModel.user == null) { 138 | navController.navigate("login") 139 | } else { 140 | navController.navigate("collect") 141 | } 142 | } 143 | ) { 144 | Spacer(modifier = Modifier.width(16.dp)) 145 | Text( 146 | text = "我的收藏", 147 | modifier = Modifier 148 | .align(Alignment.CenterVertically) 149 | .weight(1f), 150 | color = Colors.text_h1 151 | ) 152 | Icon( 153 | painter = painterResource(id = R.drawable.ic_arrow_right), 154 | contentDescription = "", 155 | modifier = Modifier 156 | .size(20.dp) 157 | .align(Alignment.CenterVertically), 158 | tint = Colors.text_h2 159 | ) 160 | Spacer(modifier = Modifier.width(16.dp)) 161 | } 162 | Spacer(modifier = Modifier.height(50.dp)) 163 | if (viewModel.user != null) { 164 | Button( 165 | onClick = { viewModel.showDialog = true }, 166 | modifier = Modifier 167 | .fillMaxWidth() 168 | .height(44.dp) 169 | .padding(horizontal = 16.dp), 170 | shape = RoundedCornerShape(percent = 50), 171 | colors = ButtonDefaults.buttonColors( 172 | containerColor = Colors.red, 173 | contentColor = Color.White 174 | ) 175 | ) { 176 | Text(text = "退出登录", fontSize = 15.sp) 177 | } 178 | } 179 | } 180 | if (viewModel.showDialog) { 181 | AlertDialog( 182 | onDismissRequest = {}, 183 | confirmButton = { 184 | TextButton( 185 | onClick = { 186 | viewModel.showDialog = false 187 | viewModel.logout() 188 | } 189 | ) { 190 | Text("确认") 191 | } 192 | }, 193 | dismissButton = { 194 | TextButton( 195 | onClick = { 196 | viewModel.showDialog = false 197 | } 198 | ) { 199 | Text("取消") 200 | } 201 | }, 202 | title = { 203 | Text(text = "提示") 204 | }, 205 | text = { 206 | Text(text = "确认退出登录?") 207 | } 208 | ) 209 | } 210 | } 211 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/mine/Register.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.mine 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.foundation.text.KeyboardOptions 7 | import androidx.compose.material.* 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.Divider 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.OutlinedTextField 12 | import androidx.compose.material3.Text 13 | import androidx.compose.material3.TextFieldDefaults 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.text.input.KeyboardType 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import androidx.lifecycle.viewmodel.compose.viewModel 21 | import androidx.navigation.NavHostController 22 | import top.wangchenyan.wancompose.theme.Colors 23 | import top.wangchenyan.wancompose.ui.mine.viewmodel.RegisterViewModel 24 | import top.wangchenyan.wancompose.widget.PageLoading 25 | import top.wangchenyan.wancompose.widget.TitleLayout 26 | 27 | @OptIn(ExperimentalMaterial3Api::class) 28 | @Composable 29 | fun Register(navController: NavHostController) { 30 | val viewModel: RegisterViewModel = viewModel() 31 | PageLoading( 32 | modifier = Modifier.background(Colors.background), 33 | showLoading = viewModel.showLoading 34 | ) { 35 | Column(Modifier.fillMaxSize()) { 36 | TitleLayout(title = "注册", onBack = { 37 | navController.popBackStack() 38 | }) 39 | Column(Modifier.fillMaxSize()) { 40 | Spacer( 41 | modifier = Modifier 42 | .height(120.dp) 43 | ) 44 | OutlinedTextField( 45 | value = viewModel.username, 46 | onValueChange = { value -> 47 | viewModel.username = value 48 | }, 49 | modifier = Modifier.fillMaxWidth(), 50 | label = { 51 | Text(text = "请输入用户名") 52 | }, 53 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), 54 | singleLine = true, 55 | colors = TextFieldDefaults.outlinedTextFieldColors( 56 | focusedBorderColor = Color.Transparent, 57 | unfocusedBorderColor = Color.Transparent 58 | ) 59 | ) 60 | Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) 61 | Spacer(modifier = Modifier.height(10.dp)) 62 | OutlinedTextField( 63 | value = viewModel.password, 64 | onValueChange = { value -> 65 | viewModel.password = value 66 | }, 67 | modifier = Modifier.fillMaxWidth(), 68 | label = { 69 | Text(text = "请输入密码") 70 | }, 71 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), 72 | singleLine = true, 73 | colors = TextFieldDefaults.outlinedTextFieldColors( 74 | focusedBorderColor = Color.Transparent, 75 | unfocusedBorderColor = Color.Transparent 76 | ) 77 | ) 78 | Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) 79 | Spacer(modifier = Modifier.height(10.dp)) 80 | OutlinedTextField( 81 | value = viewModel.repassword, 82 | onValueChange = { value -> 83 | viewModel.repassword = value 84 | }, 85 | modifier = Modifier.fillMaxWidth(), 86 | label = { 87 | Text(text = "请确认密码") 88 | }, 89 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), 90 | singleLine = true, 91 | colors = TextFieldDefaults.outlinedTextFieldColors( 92 | focusedBorderColor = Color.Transparent, 93 | unfocusedBorderColor = Color.Transparent 94 | ) 95 | ) 96 | Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) 97 | Spacer(modifier = Modifier.height(50.dp)) 98 | Button( 99 | onClick = { viewModel.register(navController) }, 100 | modifier = Modifier 101 | .fillMaxWidth() 102 | .height(44.dp) 103 | .padding(horizontal = 16.dp), 104 | shape = RoundedCornerShape(percent = 50), 105 | ) { 106 | Text(text = "注册", fontSize = 15.sp) 107 | } 108 | } 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/mine/viewmodel/CollectViewModel.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.mine.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.king.ultraswiperefresh.UltraSwipeRefreshState 9 | import kotlinx.coroutines.launch 10 | import top.wangchenyan.common.ext.toast 11 | import top.wangchenyan.common.net.apiCall 12 | import top.wangchenyan.wancompose.api.Api 13 | import top.wangchenyan.wancompose.ui.home.model.Article 14 | import top.wangchenyan.wancompose.widget.LoadState 15 | 16 | /** 17 | * Created by wcy on 2021/4/1. 18 | */ 19 | class CollectViewModel : ViewModel() { 20 | var pageState by mutableStateOf(LoadState.LOADING) 21 | var showLoading by mutableStateOf(false) 22 | var list by mutableStateOf(listOf
()) 23 | var refreshState by mutableStateOf( 24 | UltraSwipeRefreshState( 25 | isRefreshing = false, 26 | isLoading = false 27 | ) 28 | ) 29 | private var page = 0 30 | 31 | init { 32 | firstLoad() 33 | } 34 | 35 | fun firstLoad() { 36 | viewModelScope.launch { 37 | page = 0 38 | pageState = LoadState.LOADING 39 | val articleList = apiCall { Api.get().getCollectArticleList() } 40 | if (articleList.isSuccessWithData()) { 41 | pageState = LoadState.SUCCESS 42 | list = articleList.data!!.datas.onEach { it.collect = true } 43 | } else { 44 | pageState = LoadState.FAIL 45 | } 46 | } 47 | } 48 | 49 | fun onRefresh() { 50 | viewModelScope.launch { 51 | page = 0 52 | refreshState.isRefreshing = true 53 | val articleList = apiCall { Api.get().getCollectArticleList() } 54 | if (articleList.isSuccessWithData()) { 55 | list = articleList.data!!.datas.onEach { it.collect = true } 56 | refreshState.isRefreshing = false 57 | } else { 58 | refreshState.isRefreshing = false 59 | toast("加载失败") 60 | } 61 | } 62 | } 63 | 64 | fun onLoad() { 65 | viewModelScope.launch { 66 | refreshState.isLoading = true 67 | val articleList = apiCall { Api.get().getCollectArticleList(page + 1) } 68 | if (articleList.isSuccessWithData()) { 69 | page++ 70 | list = list.toMutableList().apply { 71 | addAll(articleList.data!!.datas.onEach { it.collect = true }) 72 | } 73 | refreshState.isLoading = false 74 | } else { 75 | refreshState.isLoading = false 76 | toast("加载失败") 77 | } 78 | } 79 | } 80 | 81 | fun uncollect(article: Article) { 82 | viewModelScope.launch { 83 | showLoading = true 84 | val res = collect(article) { it.originId } 85 | if (res) { 86 | onRefresh() 87 | } 88 | showLoading = false 89 | } 90 | } 91 | 92 | companion object { 93 | suspend fun collect( 94 | article: Article, 95 | getId: (article: Article) -> Long = { it.id } 96 | ): Boolean { 97 | val id = getId.invoke(article) 98 | if (article.collect) { 99 | val res = apiCall { Api.get().uncollect(id) } 100 | if (res.isSuccess()) { 101 | article.collect = false 102 | return true 103 | } else { 104 | toast(res.msg) 105 | } 106 | } else { 107 | val res = apiCall { Api.get().collect(id) } 108 | if (res.isSuccess()) { 109 | article.collect = true 110 | return true 111 | } else { 112 | toast(res.msg) 113 | } 114 | } 115 | return false 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/mine/viewmodel/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.mine.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import androidx.navigation.NavHostController 9 | import kotlinx.coroutines.launch 10 | import top.wangchenyan.common.ext.toast 11 | import top.wangchenyan.common.net.apiCall 12 | import top.wangchenyan.wancompose.api.Api 13 | import top.wangchenyan.wancompose.auth.AuthManager 14 | 15 | class LoginViewModel : ViewModel() { 16 | var showLoading by mutableStateOf(false) 17 | var username by mutableStateOf("") 18 | var password by mutableStateOf("") 19 | 20 | fun login(navController: NavHostController) { 21 | viewModelScope.launch { 22 | if (username.isEmpty()) { 23 | toast("请输入用户名") 24 | return@launch 25 | } 26 | if (password.isEmpty()) { 27 | toast("请输入密码") 28 | return@launch 29 | } 30 | showLoading = true 31 | val loginRes = apiCall { Api.get().login(username, password) } 32 | showLoading = false 33 | if (loginRes.isSuccessWithData()) { 34 | AuthManager.onLogin(loginRes.data!!) 35 | navController.popBackStack() 36 | toast("登录成功") 37 | } else { 38 | toast(loginRes.msg) 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/mine/viewmodel/MineViewModel.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.mine.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import kotlinx.coroutines.launch 9 | import top.wangchenyan.common.net.apiCall 10 | import top.wangchenyan.wancompose.api.Api 11 | import top.wangchenyan.wancompose.auth.AuthManager 12 | 13 | class MineViewModel : ViewModel() { 14 | var user by mutableStateOf(AuthManager.user.value) 15 | var showLoading by mutableStateOf(false) 16 | var showDialog by mutableStateOf(false) 17 | 18 | init { 19 | AuthManager.user.observeForever { 20 | user = it?.copy() 21 | } 22 | } 23 | 24 | fun logout() { 25 | viewModelScope.launch { 26 | showLoading = true 27 | apiCall { Api.get().logout() } 28 | showLoading = false 29 | AuthManager.onLogout() 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/mine/viewmodel/RegisterViewModel.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.mine.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import androidx.navigation.NavHostController 9 | import kotlinx.coroutines.launch 10 | import top.wangchenyan.common.ext.toast 11 | import top.wangchenyan.common.net.apiCall 12 | import top.wangchenyan.wancompose.api.Api 13 | 14 | class RegisterViewModel : ViewModel() { 15 | var showLoading by mutableStateOf(false) 16 | var username by mutableStateOf("") 17 | var password by mutableStateOf("") 18 | var repassword by mutableStateOf("") 19 | 20 | fun register(navController: NavHostController) { 21 | viewModelScope.launch { 22 | if (username.isEmpty()) { 23 | toast("请输入用户名") 24 | return@launch 25 | } 26 | if (password.isEmpty()) { 27 | toast("请输入密码") 28 | return@launch 29 | } 30 | if (repassword.isEmpty()) { 31 | toast("请确认密码") 32 | return@launch 33 | } 34 | showLoading = true 35 | val registerRes = apiCall { Api.get().register(username, password, repassword) } 36 | showLoading = false 37 | if (registerRes.isSuccess()) { 38 | navController.popBackStack() 39 | toast("注册成功") 40 | } else { 41 | toast(registerRes.msg) 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/search/Search.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.search 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.foundation.lazy.LazyColumn 15 | import androidx.compose.foundation.lazy.itemsIndexed 16 | import androidx.compose.foundation.shape.RoundedCornerShape 17 | import androidx.compose.foundation.text.BasicTextField 18 | import androidx.compose.material3.Icon 19 | import androidx.compose.material3.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.res.painterResource 25 | import androidx.compose.ui.text.TextStyle 26 | import androidx.compose.ui.unit.dp 27 | import androidx.compose.ui.unit.sp 28 | import androidx.lifecycle.viewmodel.compose.viewModel 29 | import androidx.navigation.NavHostController 30 | import com.google.accompanist.flowlayout.FlowRow 31 | import top.wangchenyan.common.ext.toast 32 | import top.wangchenyan.wancompose.R 33 | import top.wangchenyan.wancompose.theme.Colors 34 | import top.wangchenyan.wancompose.ui.search.viewmodel.SearchViewModel 35 | 36 | @Composable 37 | fun Search(navController: NavHostController) { 38 | val viewModel: SearchViewModel = viewModel() 39 | Column( 40 | modifier = Modifier 41 | .fillMaxSize() 42 | .background(Colors.background) 43 | ) { 44 | Row( 45 | Modifier 46 | .fillMaxWidth() 47 | .height(48.dp) 48 | .background(Colors.titleBar) 49 | ) { 50 | Icon( 51 | modifier = Modifier 52 | .align(alignment = Alignment.CenterVertically) 53 | .clickable { 54 | navController.popBackStack() 55 | } 56 | .size(48.dp) 57 | .padding(14.dp), 58 | painter = painterResource(id = R.drawable.ic_back), 59 | contentDescription = "返回", 60 | tint = Colors.text_h1 61 | ) 62 | BasicTextField( 63 | value = viewModel.keyword, 64 | onValueChange = { 65 | viewModel.keyword = it 66 | }, 67 | modifier = Modifier 68 | .align(alignment = Alignment.CenterVertically) 69 | .padding(start = 16.dp, end = 16.dp) 70 | .weight(1f), 71 | textStyle = TextStyle(fontSize = 16.sp), 72 | singleLine = true 73 | ) 74 | Icon( 75 | modifier = Modifier 76 | .align(alignment = Alignment.CenterVertically) 77 | .clickable { 78 | viewModel.keyword = "" 79 | } 80 | .size(48.dp) 81 | .padding(14.dp), 82 | painter = painterResource(id = R.drawable.ic_close), 83 | contentDescription = "清除", 84 | tint = Colors.text_h1 85 | ) 86 | Icon( 87 | modifier = Modifier 88 | .align(alignment = Alignment.CenterVertically) 89 | .clickable { 90 | if (viewModel.keyword.isNotEmpty()) { 91 | viewModel.addHistory(viewModel.keyword) 92 | navController.navigate("search_result?keyword=${viewModel.keyword}") 93 | } else { 94 | toast("请输入关键字") 95 | } 96 | } 97 | .size(48.dp) 98 | .padding(14.dp), 99 | painter = painterResource(id = R.drawable.ic_search), 100 | contentDescription = "", 101 | tint = Colors.text_h1 102 | ) 103 | } 104 | Column( 105 | modifier = Modifier 106 | .fillMaxSize() 107 | ) { 108 | Spacer(modifier = Modifier.height(16.dp)) 109 | Text( 110 | text = "热门搜索", 111 | modifier = Modifier.padding(horizontal = 16.dp), 112 | color = Colors.text_h1, 113 | fontSize = 16.sp 114 | ) 115 | Spacer(modifier = Modifier.height(16.dp)) 116 | FlowRow( 117 | modifier = Modifier 118 | .fillMaxWidth() 119 | .padding(horizontal = 16.dp), 120 | mainAxisSpacing = 8.dp, 121 | crossAxisSpacing = 8.dp, 122 | ) { 123 | viewModel.hotKeys.forEach { 124 | Box( 125 | modifier = Modifier 126 | .background( 127 | color = Color(0xFFDDDDDD), 128 | shape = RoundedCornerShape(percent = 50) 129 | ) 130 | .clickable { 131 | viewModel.keyword = it.name 132 | viewModel.addHistory(viewModel.keyword) 133 | navController.navigate("search_result?keyword=${viewModel.keyword}") 134 | } 135 | ) { 136 | Text( 137 | text = it.name, 138 | modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), 139 | color = Colors.text_h1, 140 | fontSize = 14.sp, 141 | maxLines = 1 142 | ) 143 | } 144 | } 145 | } 146 | Spacer(modifier = Modifier.height(16.dp)) 147 | Text( 148 | text = "搜索历史", 149 | modifier = Modifier.padding(horizontal = 16.dp), 150 | color = Colors.text_h1, 151 | fontSize = 16.sp 152 | ) 153 | Spacer(modifier = Modifier.height(16.dp)) 154 | LazyColumn(modifier = Modifier.fillMaxSize()) { 155 | itemsIndexed(viewModel.history) { index, item -> 156 | Box(modifier = Modifier 157 | .fillMaxWidth() 158 | .clickable { 159 | viewModel.keyword = item 160 | navController.navigate("search_result?keyword=${viewModel.keyword}") 161 | }) { 162 | Row( 163 | modifier = Modifier 164 | .fillMaxWidth() 165 | .padding(horizontal = 16.dp, vertical = 12.dp) 166 | ) { 167 | Text( 168 | text = item, 169 | modifier = Modifier 170 | .weight(1f) 171 | .align(Alignment.CenterVertically), 172 | color = Colors.text_h2, 173 | fontSize = 14.sp, 174 | maxLines = 1 175 | ) 176 | Icon( 177 | painter = painterResource(id = R.drawable.ic_close), 178 | contentDescription = "删除", 179 | modifier = Modifier 180 | .size(20.dp) 181 | .clickable { 182 | viewModel.removeHistory(item) 183 | }, 184 | tint = Colors.text_h2 185 | ) 186 | } 187 | } 188 | } 189 | } 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/search/SearchResult.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.search 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.foundation.lazy.itemsIndexed 9 | import androidx.compose.material3.Divider 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import androidx.lifecycle.viewmodel.compose.viewModel 14 | import androidx.navigation.NavHostController 15 | import com.king.ultraswiperefresh.NestedScrollMode 16 | import com.king.ultraswiperefresh.UltraSwipeRefresh 17 | import top.wangchenyan.wancompose.theme.Colors 18 | import top.wangchenyan.wancompose.ui.home.ArticleItem 19 | import top.wangchenyan.wancompose.ui.search.viewmodel.SearchResultViewModel 20 | import top.wangchenyan.wancompose.widget.PageLoading 21 | import top.wangchenyan.wancompose.widget.TitleLayout 22 | 23 | @Composable 24 | fun SearchResult(navController: NavHostController, keyword: String) { 25 | val viewModel: SearchResultViewModel = viewModel() 26 | viewModel.setKeyword(keyword) 27 | Column( 28 | Modifier 29 | .fillMaxSize() 30 | .background(Colors.background) 31 | ) { 32 | TitleLayout(title = keyword, onBack = { 33 | navController.popBackStack() 34 | }) 35 | PageLoading( 36 | loadState = viewModel.pageState, 37 | onReload = { viewModel.firstLoad() }, 38 | showLoading = viewModel.showLoading 39 | ) { 40 | UltraSwipeRefresh( 41 | state = viewModel.refreshState, 42 | onRefresh = { viewModel.onRefresh() }, 43 | onLoadMore = { viewModel.onLoad() }, 44 | headerScrollMode = NestedScrollMode.FixedContent, 45 | footerScrollMode = NestedScrollMode.FixedContent, 46 | ) { 47 | LazyColumn( 48 | Modifier 49 | .fillMaxSize() 50 | .background(Colors.white) 51 | ) { 52 | itemsIndexed(viewModel.list) { index, item -> 53 | ArticleItem(navController, item) { 54 | viewModel.collect(item) 55 | } 56 | Divider(Modifier.padding(16.dp, 0.dp), thickness = 0.5.dp) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/search/model/SearchData.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.search.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class HotKey( 6 | @SerializedName("id") val id: Long = 0, 7 | @SerializedName("link") val link: String = "", 8 | @SerializedName("name") val name: String = "" 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/search/viewmodel/SearchResultViewModel.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.search.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.core.text.HtmlCompat 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import com.king.ultraswiperefresh.UltraSwipeRefreshState 10 | import kotlinx.coroutines.launch 11 | import top.wangchenyan.common.ext.toast 12 | import top.wangchenyan.common.net.apiCall 13 | import top.wangchenyan.wancompose.api.Api 14 | import top.wangchenyan.wancompose.ui.home.model.Article 15 | import top.wangchenyan.wancompose.ui.mine.viewmodel.CollectViewModel 16 | import top.wangchenyan.wancompose.widget.LoadState 17 | 18 | class SearchResultViewModel : ViewModel() { 19 | var pageState by mutableStateOf(LoadState.LOADING) 20 | var showLoading by mutableStateOf(false) 21 | var list by mutableStateOf(listOf
()) 22 | var refreshState by mutableStateOf( 23 | UltraSwipeRefreshState( 24 | isRefreshing = false, 25 | isLoading = false 26 | ) 27 | ) 28 | private var keyword = "" 29 | private var page = 0 30 | 31 | fun setKeyword(keyword: String) { 32 | if (this.keyword.isEmpty()) { 33 | this.keyword = keyword 34 | firstLoad() 35 | } 36 | } 37 | 38 | fun firstLoad() { 39 | viewModelScope.launch { 40 | page = 0 41 | pageState = LoadState.LOADING 42 | val articleList = apiCall { Api.get().search(page, keyword) } 43 | if (articleList.isSuccessWithData()) { 44 | pageState = LoadState.SUCCESS 45 | if (articleList.getDataOrThrow().datas.isNotEmpty()) { 46 | list = articleList.getDataOrThrow().datas.onEach { it.setSpannableTitle() } 47 | } else { 48 | pageState = LoadState.EMPTY 49 | } 50 | } else { 51 | pageState = LoadState.FAIL 52 | } 53 | } 54 | } 55 | 56 | fun onRefresh() { 57 | viewModelScope.launch { 58 | page = 0 59 | refreshState.isRefreshing = true 60 | val articleList = apiCall { Api.get().search(page, keyword) } 61 | if (articleList.isSuccessWithData()) { 62 | list = articleList.getDataOrThrow().datas.onEach { it.setSpannableTitle() } 63 | refreshState.isRefreshing = false 64 | } else { 65 | refreshState.isRefreshing = false 66 | toast("加载失败") 67 | } 68 | } 69 | } 70 | 71 | fun onLoad() { 72 | viewModelScope.launch { 73 | refreshState.isLoading = true 74 | val articleList = apiCall { Api.get().search(page + 1, keyword) } 75 | if (articleList.isSuccessWithData()) { 76 | page++ 77 | list = list.toMutableList().apply { 78 | addAll(articleList.getDataOrThrow().datas.onEach { it.setSpannableTitle() }) 79 | } 80 | refreshState.isLoading = false 81 | } else { 82 | refreshState.isLoading = false 83 | toast("加载失败") 84 | } 85 | } 86 | } 87 | 88 | fun collect(article: Article) { 89 | viewModelScope.launch { 90 | showLoading = true 91 | CollectViewModel.collect(article) 92 | showLoading = false 93 | } 94 | } 95 | 96 | private fun Article.setSpannableTitle() { 97 | val htmlTitle = this.title.replace(Regex("(\\w+)")) { matchResult -> 98 | val title = matchResult.groupValues.getOrElse(1) { 99 | matchResult.groupValues.firstOrNull() ?: "" 100 | } 101 | "${title}" 102 | } 103 | this.setSpannableTitle( 104 | HtmlCompat.fromHtml( 105 | htmlTitle, 106 | HtmlCompat.FROM_HTML_MODE_COMPACT 107 | ) 108 | ) 109 | } 110 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/search/viewmodel/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.search.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import kotlinx.coroutines.launch 9 | import top.wangchenyan.common.net.apiCall 10 | import top.wangchenyan.wancompose.api.Api 11 | import top.wangchenyan.wancompose.storage.HistoryPreferences 12 | import top.wangchenyan.wancompose.ui.search.model.HotKey 13 | 14 | class SearchViewModel : ViewModel() { 15 | var keyword by mutableStateOf("") 16 | var hotKeys by mutableStateOf(listOf()) 17 | var history by mutableStateOf(listOf()) 18 | 19 | init { 20 | getHotKey() 21 | getHistory() 22 | } 23 | 24 | private fun getHotKey() { 25 | viewModelScope.launch { 26 | val hotKeyRes = apiCall { Api.get().searchHotKey() } 27 | if (hotKeyRes.isSuccessWithData()) { 28 | hotKeys = hotKeyRes.data!! 29 | } 30 | } 31 | } 32 | 33 | private fun getHistory() { 34 | viewModelScope.launch { 35 | history = HistoryPreferences.getHistory() 36 | } 37 | } 38 | 39 | fun addHistory(item: String) { 40 | viewModelScope.launch { 41 | if (item.isEmpty() || history.contains(item)) { 42 | return@launch 43 | } 44 | history = history.toMutableList().apply { add(0, item) } 45 | HistoryPreferences.addHistory(item) 46 | } 47 | } 48 | 49 | fun removeHistory(item: String) { 50 | viewModelScope.launch { 51 | if (item.isEmpty() || !history.contains(item)) { 52 | return@launch 53 | } 54 | history = history.toMutableList().apply { remove(item) } 55 | HistoryPreferences.removeHistory(item) 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/square/Square.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.square 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.foundation.lazy.itemsIndexed 9 | import androidx.compose.material3.Divider 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import androidx.lifecycle.viewmodel.compose.viewModel 14 | import androidx.navigation.NavHostController 15 | import com.king.ultraswiperefresh.NestedScrollMode 16 | import com.king.ultraswiperefresh.UltraSwipeRefresh 17 | import top.wangchenyan.common.ext.toast 18 | import top.wangchenyan.wancompose.R 19 | import top.wangchenyan.wancompose.theme.Colors 20 | import top.wangchenyan.wancompose.ui.home.ArticleItem 21 | import top.wangchenyan.wancompose.ui.square.viewmodel.SquareViewModel 22 | import top.wangchenyan.wancompose.widget.PageLoading 23 | import top.wangchenyan.wancompose.widget.TitleLayout 24 | 25 | /** 26 | * Created by wcy on 2021/3/31. 27 | */ 28 | 29 | @Composable 30 | fun Square(navController: NavHostController) { 31 | val viewModel: SquareViewModel = viewModel() 32 | Column( 33 | Modifier 34 | .fillMaxSize() 35 | .background(Colors.background) 36 | ) { 37 | TitleLayout( 38 | title = "广场", 39 | menuIcon = R.drawable.ic_share, 40 | onMenuClick = { 41 | toast("分享") 42 | } 43 | ) 44 | PageLoading( 45 | loadState = viewModel.pageState, 46 | onReload = { viewModel.firstLoad() }, 47 | showLoading = viewModel.showLoading 48 | ) { 49 | UltraSwipeRefresh( 50 | state = viewModel.refreshState, 51 | onRefresh = { viewModel.onRefresh() }, 52 | onLoadMore = { viewModel.onLoad() }, 53 | headerScrollMode = NestedScrollMode.FixedContent, 54 | footerScrollMode = NestedScrollMode.FixedContent, 55 | ) { 56 | LazyColumn( 57 | Modifier 58 | .fillMaxSize() 59 | .background(Colors.white) 60 | ) { 61 | itemsIndexed(viewModel.list) { index, item -> 62 | ArticleItem(navController, item) { 63 | viewModel.collect(item) 64 | } 65 | Divider(Modifier.padding(16.dp, 0.dp), thickness = 0.5.dp) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/square/viewmodel/SquareViewModel.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.square.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.king.ultraswiperefresh.UltraSwipeRefreshState 9 | import kotlinx.coroutines.launch 10 | import top.wangchenyan.common.ext.toast 11 | import top.wangchenyan.common.net.apiCall 12 | import top.wangchenyan.wancompose.api.Api 13 | import top.wangchenyan.wancompose.ui.home.model.Article 14 | import top.wangchenyan.wancompose.ui.mine.viewmodel.CollectViewModel 15 | import top.wangchenyan.wancompose.widget.LoadState 16 | 17 | /** 18 | * Created by wcy on 2021/4/1. 19 | */ 20 | class SquareViewModel : ViewModel() { 21 | var pageState by mutableStateOf(LoadState.LOADING) 22 | var showLoading by mutableStateOf(false) 23 | var list by mutableStateOf(listOf
()) 24 | var refreshState by mutableStateOf( 25 | UltraSwipeRefreshState( 26 | isRefreshing = false, 27 | isLoading = false 28 | ) 29 | ) 30 | private var page = 0 31 | 32 | init { 33 | firstLoad() 34 | } 35 | 36 | fun firstLoad() { 37 | viewModelScope.launch { 38 | page = 0 39 | pageState = LoadState.LOADING 40 | val articleList = apiCall { Api.get().getSquareArticleList() } 41 | if (articleList.isSuccessWithData()) { 42 | pageState = LoadState.SUCCESS 43 | list = articleList.data!!.datas 44 | } else { 45 | pageState = LoadState.FAIL 46 | } 47 | } 48 | } 49 | 50 | fun onRefresh() { 51 | viewModelScope.launch { 52 | page = 0 53 | refreshState.isRefreshing = true 54 | val articleList = apiCall { Api.get().getSquareArticleList() } 55 | if (articleList.isSuccessWithData()) { 56 | list = articleList.data!!.datas 57 | refreshState.isRefreshing = false 58 | } else { 59 | refreshState.isRefreshing = false 60 | toast("加载失败") 61 | } 62 | } 63 | } 64 | 65 | fun onLoad() { 66 | viewModelScope.launch { 67 | refreshState.isLoading = true 68 | val articleList = apiCall { Api.get().getSquareArticleList(page + 1) } 69 | if (articleList.isSuccessWithData()) { 70 | page++ 71 | list = list.toMutableList().apply { 72 | addAll(articleList.data!!.datas) 73 | } 74 | refreshState.isLoading = false 75 | } else { 76 | refreshState.isLoading = false 77 | toast("加载失败") 78 | } 79 | } 80 | } 81 | 82 | fun collect(article: Article) { 83 | viewModelScope.launch { 84 | showLoading = true 85 | CollectViewModel.collect(article) 86 | showLoading = false 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/web/Web.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.web 2 | 3 | import android.content.Intent 4 | import android.content.pm.PackageManager 5 | import android.net.Uri 6 | import android.net.http.SslError 7 | import android.os.Build 8 | import android.text.TextUtils 9 | import android.view.ViewGroup 10 | import android.webkit.SslErrorHandler 11 | import android.webkit.WebChromeClient 12 | import android.webkit.WebSettings 13 | import android.webkit.WebView 14 | import android.webkit.WebViewClient 15 | import androidx.compose.foundation.background 16 | import androidx.compose.foundation.layout.Box 17 | import androidx.compose.foundation.layout.Column 18 | import androidx.compose.foundation.layout.fillMaxSize 19 | import androidx.compose.foundation.layout.fillMaxWidth 20 | import androidx.compose.foundation.layout.height 21 | import androidx.compose.material3.LinearProgressIndicator 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.viewinterop.AndroidView 27 | import androidx.lifecycle.viewmodel.compose.viewModel 28 | import androidx.navigation.NavHostController 29 | import top.wangchenyan.wancompose.ui.web.viewmodel.WebViewModel 30 | import top.wangchenyan.wancompose.widget.TitleLayout 31 | 32 | @Composable 33 | fun Web(navController: NavHostController, url: String) { 34 | val viewModel: WebViewModel = viewModel() 35 | Column( 36 | Modifier 37 | .fillMaxSize() 38 | .background(Color.White) 39 | ) { 40 | TitleLayout(title = viewModel.title, onBack = { 41 | navController.popBackStack() 42 | }) 43 | Box(Modifier.fillMaxSize()) { 44 | AndroidView({ context -> 45 | WebView(context).apply { 46 | layoutParams = ViewGroup.LayoutParams( 47 | ViewGroup.LayoutParams.MATCH_PARENT, 48 | ViewGroup.LayoutParams.MATCH_PARENT 49 | ) 50 | settings.apply { 51 | domStorageEnabled = true 52 | databaseEnabled = true 53 | allowFileAccess = false 54 | savePassword = false 55 | javaScriptEnabled = true 56 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 57 | mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW 58 | } 59 | } 60 | webViewClient = object : WebViewClient() { 61 | override fun shouldOverrideUrlLoading( 62 | view: WebView?, 63 | url: String? 64 | ): Boolean { 65 | if (url == null) { 66 | return false 67 | } 68 | 69 | if (url.startsWith("http://", true) 70 | || url.startsWith("https://", true) 71 | || url.startsWith("ftp://", true) 72 | ) { 73 | view?.loadUrl(url) 74 | } else { 75 | val uri = Uri.parse(url) 76 | if (!TextUtils.isEmpty(uri.scheme) && !TextUtils.isEmpty(uri.host)) { 77 | val intent = Intent(Intent.ACTION_VIEW, uri) 78 | if (isInstall(intent)) { 79 | try { 80 | context.startActivity(intent) 81 | } catch (e: Exception) { 82 | } 83 | } 84 | } 85 | } 86 | 87 | return true 88 | } 89 | 90 | override fun onReceivedSslError( 91 | view: WebView?, 92 | handler: SslErrorHandler?, 93 | error: SslError? 94 | ) { 95 | handler?.proceed() 96 | } 97 | 98 | private fun isInstall(intent: Intent): Boolean { 99 | return context.packageManager.queryIntentActivities( 100 | intent, 101 | PackageManager.MATCH_DEFAULT_ONLY 102 | ).size > 0 103 | } 104 | } 105 | webChromeClient = object : WebChromeClient() { 106 | override fun onReceivedTitle(view: WebView?, title: String?) { 107 | viewModel.title = title ?: "" 108 | } 109 | 110 | override fun onProgressChanged(view: WebView?, newProgress: Int) { 111 | super.onProgressChanged(view, newProgress) 112 | viewModel.progress = newProgress 113 | } 114 | } 115 | loadUrl(url) 116 | } 117 | }, Modifier.fillMaxSize()) 118 | if (viewModel.progress < 100) { 119 | LinearProgressIndicator( 120 | progress = viewModel.progress / 100f, 121 | modifier = Modifier 122 | .fillMaxWidth() 123 | .height(2.dp), 124 | color = Color.Transparent 125 | ) 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/web/viewmodel/WebViewModel.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.web.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | 8 | class WebViewModel : ViewModel() { 9 | var title by mutableStateOf("加载中...") 10 | var progress by mutableStateOf(0) 11 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/wechat/WeChat.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.wechat 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.lazy.LazyColumn 9 | import androidx.compose.foundation.lazy.itemsIndexed 10 | import androidx.compose.material3.Divider 11 | import androidx.compose.material3.ScrollableTabRow 12 | import androidx.compose.material3.Tab 13 | import androidx.compose.material3.TabRowDefaults 14 | import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.rememberCoroutineScope 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | import androidx.lifecycle.viewModelScope 22 | import androidx.lifecycle.viewmodel.compose.viewModel 23 | import androidx.navigation.NavHostController 24 | import com.google.accompanist.pager.ExperimentalPagerApi 25 | import com.google.accompanist.pager.HorizontalPager 26 | import com.google.accompanist.pager.rememberPagerState 27 | import com.king.ultraswiperefresh.NestedScrollMode 28 | import com.king.ultraswiperefresh.UltraSwipeRefresh 29 | import kotlinx.coroutines.launch 30 | import top.wangchenyan.wancompose.theme.Colors 31 | import top.wangchenyan.wancompose.ui.home.ArticleItem 32 | import top.wangchenyan.wancompose.ui.wechat.viewmodel.WeChatTabViewModel 33 | import top.wangchenyan.wancompose.ui.wechat.viewmodel.WeChatViewModel 34 | import top.wangchenyan.wancompose.widget.PageLoading 35 | import top.wangchenyan.wancompose.widget.TitleLayout 36 | 37 | /** 38 | * Created by wcy on 2021/3/31. 39 | */ 40 | 41 | @ExperimentalPagerApi 42 | @Composable 43 | fun WeChat(navController: NavHostController) { 44 | val viewModel: WeChatViewModel = viewModel() 45 | Column( 46 | Modifier 47 | .fillMaxSize() 48 | .background(Colors.background) 49 | ) { 50 | TitleLayout(title = "公众号") 51 | PageLoading( 52 | loadState = viewModel.pageState, 53 | onReload = { viewModel.getAuthorList() }) { 54 | if (viewModel.authorList.isNotEmpty()) { 55 | val scope = rememberCoroutineScope() 56 | val pagerState = rememberPagerState( 57 | pageCount = viewModel.authorList.size, 58 | initialOffscreenLimit = viewModel.authorList.size - 1 59 | ) 60 | Column(Modifier.fillMaxSize()) { 61 | ScrollableTabRow( 62 | selectedTabIndex = pagerState.currentPage, 63 | modifier = Modifier 64 | .fillMaxWidth(), 65 | containerColor = Colors.titleBar, 66 | indicator = { tabPositions -> 67 | TabRowDefaults.Indicator( 68 | modifier = Modifier 69 | .tabIndicatorOffset(tabPositions[pagerState.currentPage]) 70 | .padding(start = 20.dp, end = 20.dp), 71 | color = Colors.main 72 | ) 73 | }, 74 | divider = {} 75 | ) { 76 | viewModel.authorList.forEachIndexed { index, weChatAuthor -> 77 | Tab( 78 | modifier = Modifier.padding(vertical = 10.dp), 79 | selected = (index == pagerState.currentPage), 80 | onClick = { 81 | scope.launch { 82 | pagerState.scrollToPage(index) 83 | } 84 | }) { 85 | Text(text = weChatAuthor.name, fontSize = 16.sp) 86 | } 87 | } 88 | } 89 | HorizontalPager( 90 | state = pagerState, 91 | modifier = Modifier.fillMaxSize(), 92 | ) { 93 | WeChatTab( 94 | navController, 95 | viewModel, 96 | viewModel.authorList[currentPage].id 97 | ) 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | @Composable 106 | fun WeChatTab(navController: NavHostController, viewModel: WeChatViewModel, id: Long) { 107 | var tabViewModel = viewModel.tabViewModelMap[id] 108 | if (tabViewModel == null) { 109 | tabViewModel = WeChatTabViewModel(viewModel.viewModelScope, id) 110 | viewModel.tabViewModelMap.put(id, tabViewModel) 111 | } 112 | Column(Modifier.fillMaxSize()) { 113 | PageLoading( 114 | loadState = tabViewModel.pageState, 115 | onReload = { tabViewModel.firstLoad() }, 116 | showLoading = tabViewModel.showLoading 117 | ) { 118 | UltraSwipeRefresh( 119 | state = tabViewModel.refreshState, 120 | refreshEnabled = false, 121 | loadMoreEnabled = true, 122 | onRefresh = {}, 123 | onLoadMore = { tabViewModel.loadArticleList() }, 124 | headerScrollMode = NestedScrollMode.FixedContent, 125 | footerScrollMode = NestedScrollMode.FixedContent, 126 | ) { 127 | LazyColumn( 128 | Modifier 129 | .fillMaxSize() 130 | .background(Colors.white) 131 | ) { 132 | itemsIndexed(tabViewModel.articleList) { index, item -> 133 | ArticleItem(navController, item) { 134 | tabViewModel.collect(item) 135 | } 136 | Divider(Modifier.padding(16.dp, 0.dp), thickness = 0.5.dp) 137 | } 138 | } 139 | } 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/wechat/model/WeChatData.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.wechat.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class WeChatAuthor( 6 | @SerializedName("id") val id: Long = 0, 7 | @SerializedName("name") val name: String = "" 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/wechat/viewmodel/WeChatTabViewModel.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.wechat.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import com.king.ultraswiperefresh.UltraSwipeRefreshState 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.launch 9 | import top.wangchenyan.common.ext.toast 10 | import top.wangchenyan.common.net.apiCall 11 | import top.wangchenyan.wancompose.api.Api 12 | import top.wangchenyan.wancompose.ui.home.model.Article 13 | import top.wangchenyan.wancompose.ui.mine.viewmodel.CollectViewModel 14 | import top.wangchenyan.wancompose.widget.LoadState 15 | 16 | class WeChatTabViewModel(private val scope: CoroutineScope, private val id: Long) { 17 | var pageState by mutableStateOf(LoadState.LOADING) 18 | var showLoading by mutableStateOf(false) 19 | var articleList by mutableStateOf(listOf
()) 20 | var refreshState by mutableStateOf( 21 | UltraSwipeRefreshState( 22 | isRefreshing = false, 23 | isLoading = false 24 | ) 25 | ) 26 | private var page = 0 27 | 28 | init { 29 | firstLoad() 30 | } 31 | 32 | fun firstLoad() { 33 | scope.launch { 34 | page = 0 35 | pageState = LoadState.LOADING 36 | val res = apiCall { Api.get().getWeChatArticleList(id) } 37 | if (res.isSuccessWithData()) { 38 | pageState = LoadState.SUCCESS 39 | articleList = res.data!!.datas 40 | } else { 41 | pageState = LoadState.FAIL 42 | } 43 | } 44 | } 45 | 46 | fun loadArticleList() { 47 | scope.launch { 48 | refreshState.isLoading = true 49 | val res = apiCall { Api.get().getWeChatArticleList(id, page + 1) } 50 | if (res.isSuccessWithData()) { 51 | page++ 52 | articleList = articleList.toMutableList().apply { 53 | addAll(res.data!!.datas) 54 | } 55 | refreshState.isLoading = false 56 | } else { 57 | refreshState.isLoading = false 58 | toast("加载失败") 59 | } 60 | } 61 | } 62 | 63 | fun collect(article: Article) { 64 | scope.launch { 65 | showLoading = true 66 | CollectViewModel.collect(article) 67 | showLoading = false 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/ui/wechat/viewmodel/WeChatViewModel.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.ui.wechat.viewmodel 2 | 3 | import android.util.LongSparseArray 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import kotlinx.coroutines.launch 10 | import top.wangchenyan.common.net.apiCall 11 | import top.wangchenyan.wancompose.api.Api 12 | import top.wangchenyan.wancompose.ui.wechat.model.WeChatAuthor 13 | import top.wangchenyan.wancompose.widget.LoadState 14 | 15 | class WeChatViewModel : ViewModel() { 16 | var pageState by mutableStateOf(LoadState.LOADING) 17 | var authorList by mutableStateOf(listOf()) 18 | val tabViewModelMap = LongSparseArray() 19 | 20 | init { 21 | getAuthorList() 22 | } 23 | 24 | fun getAuthorList() { 25 | viewModelScope.launch { 26 | pageState = LoadState.LOADING 27 | val authorListRes = apiCall { Api.get().getWeChatAuthorList() } 28 | if (authorListRes.isSuccessWithData()) { 29 | pageState = LoadState.SUCCESS 30 | authorList = authorListRes.data!! 31 | } else { 32 | pageState = LoadState.FAIL 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/widget/Banner.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.widget 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Handler 5 | import android.os.Looper 6 | import androidx.compose.foundation.Image 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.* 10 | import androidx.compose.foundation.shape.CircleShape 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.rememberCoroutineScope 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.clip 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.layout.ContentScale 20 | import androidx.compose.ui.text.style.TextOverflow 21 | import androidx.compose.ui.unit.dp 22 | import androidx.compose.ui.unit.sp 23 | import androidx.navigation.NavHostController 24 | import com.google.accompanist.coil.rememberCoilPainter 25 | import com.google.accompanist.pager.ExperimentalPagerApi 26 | import com.google.accompanist.pager.HorizontalPager 27 | import com.google.accompanist.pager.rememberPagerState 28 | import kotlinx.coroutines.launch 29 | import top.wangchenyan.wancompose.theme.Colors 30 | 31 | /** 32 | * Created by wcy on 2021/4/1. 33 | */ 34 | 35 | data class BannerData( 36 | val title: String, 37 | val imageUrl: String, 38 | val jumpUrl: String 39 | ) 40 | 41 | @SuppressLint("CoroutineCreationDuringComposition") 42 | @ExperimentalPagerApi 43 | @Composable 44 | fun Banner( 45 | navController: NavHostController, 46 | modifier: Modifier, 47 | dataList: List 48 | ) { 49 | val pagerState = 50 | rememberPagerState(pageCount = dataList.size, initialOffscreenLimit = dataList.size - 1) 51 | val handler = remember { 52 | Handler(Looper.getMainLooper()) 53 | } 54 | val scope = rememberCoroutineScope() 55 | handler.removeCallbacksAndMessages(null) 56 | handler.postDelayed(object : Runnable { 57 | override fun run() { 58 | scope.launch { 59 | if (pagerState.pageCount > 0) { 60 | pagerState.animateScrollToPage((pagerState.currentPage + 1) % pagerState.pageCount) 61 | } 62 | } 63 | handler.postDelayed(this, 3000) 64 | } 65 | }, 3000) 66 | HorizontalPager( 67 | state = pagerState, 68 | modifier = modifier 69 | ) { 70 | val bannerData = dataList[currentPage] 71 | Box(modifier = Modifier 72 | .fillMaxSize() 73 | .clickable { 74 | navController.navigate("web?url=${bannerData.jumpUrl}") 75 | }) { 76 | Image( 77 | painter = rememberCoilPainter(bannerData.imageUrl), 78 | contentDescription = null, 79 | modifier = Modifier.fillMaxSize(), 80 | contentScale = ContentScale.Crop 81 | ) 82 | Row( 83 | modifier = Modifier 84 | .fillMaxWidth() 85 | .align(Alignment.BottomCenter) 86 | .background(Color(0xFF60000000)) 87 | .padding(16.dp, 4.dp) 88 | ) { 89 | Text( 90 | text = bannerData.title, 91 | modifier = Modifier 92 | .weight(1f) 93 | .align(Alignment.CenterVertically), 94 | color = Colors.white, 95 | fontSize = 15.sp, 96 | maxLines = 1, 97 | overflow = TextOverflow.Ellipsis 98 | ) 99 | Spacer( 100 | modifier = Modifier 101 | .width(2.dp) 102 | .height(0.dp) 103 | ) 104 | for (i in dataList.indices) { 105 | Spacer( 106 | modifier = Modifier 107 | .width(8.dp) 108 | .height(0.dp) 109 | ) 110 | val color = 111 | if (i == pagerState.currentPage) Colors.white else Color.LightGray 112 | Box( 113 | modifier = Modifier 114 | .align(Alignment.CenterVertically) 115 | .width(5.dp) 116 | .height(5.dp) 117 | .clip(CircleShape) 118 | ) { 119 | Box( 120 | modifier = Modifier 121 | .fillMaxSize() 122 | .background(color) 123 | ) 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/widget/BottomTab.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.widget 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.res.painterResource 18 | import androidx.compose.ui.tooling.preview.Preview 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | import top.wangchenyan.wancompose.R 22 | import top.wangchenyan.wancompose.theme.Colors 23 | 24 | /** 25 | * Created by wcy on 2021/3/30. 26 | */ 27 | 28 | @Preview 29 | @Composable 30 | fun BottomTabPreview() { 31 | BottomTab(current = 0) { 32 | } 33 | } 34 | 35 | @Composable 36 | fun BottomTab( 37 | current: Int, 38 | currentChanged: (Int) -> Unit 39 | ) { 40 | Row( 41 | Modifier 42 | .fillMaxWidth() 43 | .background(Colors.bottomBar) 44 | .padding(4.dp, 0.dp) 45 | ) { 46 | TabItem( 47 | Modifier 48 | .weight(1f) 49 | .clickable { 50 | currentChanged(0) 51 | }, 52 | if (current == 0) R.drawable.ic_tab_home_fill else R.drawable.ic_tab_home, 53 | "首页", 54 | if (current == 0) Colors.main else Colors.unselect 55 | ) 56 | TabItem( 57 | Modifier 58 | .weight(1f) 59 | .clickable { 60 | currentChanged(1) 61 | }, 62 | if (current == 1) R.drawable.ic_tab_discover_fill else R.drawable.ic_tab_discover, 63 | "广场", 64 | if (current == 1) Colors.main else Colors.unselect 65 | ) 66 | TabItem( 67 | Modifier 68 | .weight(1f) 69 | .clickable { 70 | currentChanged(2) 71 | }, 72 | if (current == 2) R.drawable.ic_tab_wechat_fill else R.drawable.ic_tab_wechat, 73 | "公众号", 74 | if (current == 2) Colors.main else Colors.unselect 75 | ) 76 | TabItem( 77 | Modifier 78 | .weight(1f) 79 | .clickable { 80 | currentChanged(3) 81 | }, 82 | if (current == 3) R.drawable.ic_tab_my_fill else R.drawable.ic_tab_my, 83 | "我的", 84 | if (current == 3) Colors.main else Colors.unselect 85 | ) 86 | } 87 | } 88 | 89 | @Composable 90 | fun TabItem( 91 | modifier: Modifier = Modifier, 92 | @DrawableRes iconResId: Int, 93 | title: String, 94 | tint: Color 95 | ) { 96 | Column( 97 | modifier.padding(0.dp, 8.dp, 0.dp, 8.dp), 98 | horizontalAlignment = Alignment.CenterHorizontally 99 | ) { 100 | Icon(painterResource(iconResId), title, Modifier.size(24.dp), tint) 101 | Text(title, fontSize = 11.sp, color = tint) 102 | } 103 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/widget/PageLoading.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.widget 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.BoxScope 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material3.CircularProgressIndicator 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | 15 | /** 16 | * Created by wcy on 2021/4/1. 17 | */ 18 | 19 | enum class LoadState { 20 | LOADING, 21 | SUCCESS, 22 | FAIL, 23 | EMPTY 24 | } 25 | 26 | @Composable 27 | fun PageLoading( 28 | modifier: Modifier = Modifier, 29 | loadState: LoadState = LoadState.SUCCESS, 30 | onReload: () -> Unit = {}, 31 | showLoading: Boolean = false, 32 | content: @Composable BoxScope.() -> Unit 33 | ) { 34 | Box(modifier = modifier.fillMaxSize()) { 35 | when (loadState) { 36 | LoadState.LOADING -> { 37 | CircularProgressIndicator(Modifier.align(Alignment.Center)) 38 | } 39 | 40 | LoadState.SUCCESS -> { 41 | content.invoke(this) 42 | if (showLoading) { 43 | Box( 44 | modifier = Modifier 45 | .fillMaxSize() 46 | .background(Color(0x80000000)) 47 | .clickable { } 48 | ) { 49 | CircularProgressIndicator( 50 | modifier = Modifier.align(Alignment.Center), 51 | color = Color.White 52 | ) 53 | } 54 | } 55 | } 56 | 57 | LoadState.FAIL -> { 58 | Box(modifier = Modifier 59 | .fillMaxSize() 60 | .clickable { onReload.invoke() }) { 61 | Text( 62 | text = "加载失败,点击重试", 63 | Modifier.align(Alignment.Center) 64 | ) 65 | } 66 | } 67 | 68 | LoadState.EMPTY -> { 69 | Box(modifier = Modifier 70 | .fillMaxSize() 71 | .clickable { onReload.invoke() }) { 72 | Text( 73 | text = "这里什么都没有", 74 | Modifier.align(Alignment.Center) 75 | ) 76 | } 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/java/top/wangchenyan/wancompose/widget/TitleLayout.kt: -------------------------------------------------------------------------------- 1 | package top.wangchenyan.wancompose.widget 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.painterResource 17 | import androidx.compose.ui.text.style.TextOverflow 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import top.wangchenyan.wancompose.R 21 | import top.wangchenyan.wancompose.theme.Colors 22 | 23 | /** 24 | * Created by wcy on 2021/3/31. 25 | */ 26 | 27 | @Composable 28 | fun TitleLayout( 29 | title: String, 30 | onBack: (() -> Unit)? = null, 31 | @DrawableRes menuIcon: Int? = null, 32 | onMenuClick: (() -> Unit)? = null, 33 | ) { 34 | Row( 35 | Modifier 36 | .fillMaxWidth() 37 | .height(48.dp) 38 | .background(Colors.titleBar) 39 | ) { 40 | if (onBack != null) { 41 | Icon( 42 | modifier = Modifier 43 | .align(alignment = Alignment.CenterVertically) 44 | .clickable { 45 | onBack.invoke() 46 | } 47 | .size(48.dp) 48 | .padding(14.dp), 49 | painter = painterResource(id = R.drawable.ic_back), 50 | contentDescription = "返回", 51 | tint = Colors.text_h1 52 | ) 53 | } 54 | Text( 55 | text = title, 56 | modifier = Modifier 57 | .align(alignment = Alignment.CenterVertically) 58 | .padding(start = 16.dp, end = 16.dp) 59 | .weight(1f), 60 | color = Colors.text_h1, 61 | fontSize = 17.sp, 62 | maxLines = 1, 63 | overflow = TextOverflow.Ellipsis 64 | ) 65 | if (menuIcon != null) { 66 | Icon( 67 | modifier = Modifier 68 | .align(alignment = Alignment.CenterVertically) 69 | .clickable { 70 | onMenuClick?.invoke() 71 | } 72 | .size(48.dp) 73 | .padding(14.dp), 74 | painter = painterResource(id = menuIcon), 75 | contentDescription = "", 76 | tint = Colors.text_h1 77 | ) 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/res/drawable-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/res/drawable-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/res/drawable-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/res/drawable-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/res/drawable-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/android_q.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/res/drawable-xxhdpi/android_q.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/res/drawable-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/res/drawable-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/res/drawable-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangchenyan/wan-compose/468ff10e69a568455a4e5f28ad0362ccf5c9b38e/app/src/main/res/drawable-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_like.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_like_fill.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tab_discover.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 14 | 17 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tab_discover_fill.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tab_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tab_home_fill.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tab_my.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tab_my_fill.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tab_wechat.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tab_wechat_fill.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | #FF43A047 11 | #FFFAFAFA 12 | #FFFAFAFA 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 玩Compose 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 23 | 24 |