├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── kotlinScripting.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── mm │ │ └── hamcompose │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── mm │ │ │ └── hamcompose │ │ │ ├── HamApp.kt │ │ │ ├── IconsPreview.kt │ │ │ ├── data │ │ │ ├── bean │ │ │ │ ├── ConstBean.kt │ │ │ │ └── HttpBean.kt │ │ │ ├── db │ │ │ │ ├── DbConst.kt │ │ │ │ ├── history │ │ │ │ │ ├── HistoryDao.kt │ │ │ │ │ └── HistoryDatabase.kt │ │ │ │ ├── hotkey │ │ │ │ │ ├── HotKeyDatabase.kt │ │ │ │ │ └── HotkeysDao.kt │ │ │ │ └── user │ │ │ │ │ ├── UserInfoDao.kt │ │ │ │ │ └── UserInfoDatabase.kt │ │ │ ├── http │ │ │ │ ├── ApiCall.kt │ │ │ │ ├── HttpResult.kt │ │ │ │ ├── HttpService.kt │ │ │ │ ├── interceptor │ │ │ │ │ ├── CacheCookieInterceptor.kt │ │ │ │ │ ├── LogInterceptor.kt │ │ │ │ │ └── SetCookieInterceptor.kt │ │ │ │ └── paging │ │ │ │ │ ├── BasePagingSource.kt │ │ │ │ │ ├── GirlPhotoPagingSource.kt │ │ │ │ │ └── PagingFactory.kt │ │ │ └── store │ │ │ │ └── DataStoreUtils.kt │ │ │ ├── di │ │ │ ├── module │ │ │ │ ├── DatabaseModule.kt │ │ │ │ └── NetworkModule.kt │ │ │ └── scope │ │ │ │ ├── ActivityScope.kt │ │ │ │ ├── ApplicationScope.kt │ │ │ │ └── FragmentScope.kt │ │ │ ├── repository │ │ │ ├── HttpRepository.kt │ │ │ └── HttpRepositoryImpl.kt │ │ │ ├── theme │ │ │ ├── Color.kt │ │ │ ├── Dimens.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ │ ├── ui │ │ │ ├── HomeActivity.kt │ │ │ ├── HomeEntry.kt │ │ │ ├── page │ │ │ │ ├── base │ │ │ │ │ ├── BaseCollectViewModel.kt │ │ │ │ │ ├── BaseRepository.kt │ │ │ │ │ ├── BaseViewModel.kt │ │ │ │ │ ├── CacheHistoryViewModel.kt │ │ │ │ │ └── HamScaffold.kt │ │ │ │ ├── girls │ │ │ │ │ ├── info │ │ │ │ │ │ └── GirlInfoPage.kt │ │ │ │ │ └── list │ │ │ │ │ │ ├── GirlPhotoPage.kt │ │ │ │ │ │ └── GirlPhotoViewModel.kt │ │ │ │ ├── main │ │ │ │ │ ├── category │ │ │ │ │ │ ├── CategoryPage.kt │ │ │ │ │ │ ├── CategoryViewModel.kt │ │ │ │ │ │ ├── navigation │ │ │ │ │ │ │ ├── NaviPage.kt │ │ │ │ │ │ │ └── NaviViewModel.kt │ │ │ │ │ │ ├── pubaccount │ │ │ │ │ │ │ ├── author │ │ │ │ │ │ │ │ ├── PublicAccountAuthor.kt │ │ │ │ │ │ │ │ └── PublicAccountAuthorViewModel.kt │ │ │ │ │ │ │ ├── category │ │ │ │ │ │ │ │ ├── PublicAccountPage.kt │ │ │ │ │ │ │ │ └── PublicAccountViewModel.kt │ │ │ │ │ │ │ └── search │ │ │ │ │ │ │ │ ├── PublicAccountSearch.kt │ │ │ │ │ │ │ │ └── PublicAccountSearchViewModel.kt │ │ │ │ │ │ ├── share │ │ │ │ │ │ │ ├── ShareArticlePage.kt │ │ │ │ │ │ │ └── ShareArticleViewModel.kt │ │ │ │ │ │ └── structure │ │ │ │ │ │ │ ├── list │ │ │ │ │ │ │ ├── StructureListPage.kt │ │ │ │ │ │ │ └── StructureListViewModel.kt │ │ │ │ │ │ │ └── tree │ │ │ │ │ │ │ ├── StructureTreePage.kt │ │ │ │ │ │ │ └── StructureTreeViewModel.kt │ │ │ │ │ ├── collection │ │ │ │ │ │ ├── CollectionPage.kt │ │ │ │ │ │ ├── CollectionViewModel.kt │ │ │ │ │ │ └── edit │ │ │ │ │ │ │ ├── WebSiteEditPage.kt │ │ │ │ │ │ │ └── WebSiteEditViewModel.kt │ │ │ │ │ ├── home │ │ │ │ │ │ ├── HomePage.kt │ │ │ │ │ │ ├── HomeViewModel.kt │ │ │ │ │ │ ├── index │ │ │ │ │ │ │ ├── IndexPage.kt │ │ │ │ │ │ │ └── IndexViewModel.kt │ │ │ │ │ │ ├── project │ │ │ │ │ │ │ ├── ProjectPage.kt │ │ │ │ │ │ │ └── ProjectViewModel.kt │ │ │ │ │ │ ├── search │ │ │ │ │ │ │ ├── SearchPage.kt │ │ │ │ │ │ │ └── SearchViewModel.kt │ │ │ │ │ │ ├── square │ │ │ │ │ │ │ ├── SquarePage.kt │ │ │ │ │ │ │ └── SquareViewModel.kt │ │ │ │ │ │ └── wenda │ │ │ │ │ │ │ ├── WenDaPage.kt │ │ │ │ │ │ │ └── WenDaViewModel.kt │ │ │ │ │ └── profile │ │ │ │ │ │ ├── ProfilePage.kt │ │ │ │ │ │ ├── ProfileViewModel.kt │ │ │ │ │ │ ├── history │ │ │ │ │ │ ├── HistoryPage.kt │ │ │ │ │ │ └── HistoryViewModel.kt │ │ │ │ │ │ ├── message │ │ │ │ │ │ ├── MessagePage.kt │ │ │ │ │ │ └── MessageViewModel.kt │ │ │ │ │ │ ├── points │ │ │ │ │ │ ├── PointsRankingViewModel.kt │ │ │ │ │ │ └── PointsRankingsPage.kt │ │ │ │ │ │ ├── settings │ │ │ │ │ │ ├── SettingsPage.kt │ │ │ │ │ │ └── SettingsViewModel.kt │ │ │ │ │ │ ├── sharer │ │ │ │ │ │ ├── SharerPage.kt │ │ │ │ │ │ └── SharerViewModel.kt │ │ │ │ │ │ └── user │ │ │ │ │ │ ├── LoginPage.kt │ │ │ │ │ │ ├── RegisterPage.kt │ │ │ │ │ │ └── UserViewModel.kt │ │ │ │ ├── splash │ │ │ │ │ └── SplashPage.kt │ │ │ │ └── webview │ │ │ │ │ ├── WebView.kt │ │ │ │ │ └── WebViewCtrl.kt │ │ │ ├── route │ │ │ │ ├── BottomNavRoute.kt │ │ │ │ ├── RouteName.kt │ │ │ │ └── RouteUtils.kt │ │ │ └── widget │ │ │ │ ├── Animations.kt │ │ │ │ ├── AsyncImage.kt │ │ │ │ ├── Banner.kt │ │ │ │ ├── Buttons.kt │ │ │ │ ├── Common.kt │ │ │ │ ├── Dialog.kt │ │ │ │ ├── EditView.kt │ │ │ │ ├── ListItemView.kt │ │ │ │ ├── SnackBar.kt │ │ │ │ ├── SpecIcons.kt │ │ │ │ └── Title.kt │ │ │ └── util │ │ │ ├── CacheDataManager.kt │ │ │ ├── Navigation.kt │ │ │ └── RegexUtils.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── horizontal_progressbar.xml │ │ ├── ic_add.xml │ │ ├── ic_arrow_more.xml │ │ ├── ic_article.xml │ │ ├── ic_author.xml │ │ ├── ic_camera.xml │ │ ├── ic_close.xml │ │ ├── ic_community.xml │ │ ├── ic_data.xml │ │ ├── ic_delete.xml │ │ ├── ic_drawer.xml │ │ ├── ic_exit_app.xml │ │ ├── ic_feedback.xml │ │ ├── ic_help.xml │ │ ├── ic_history_record.xml │ │ ├── ic_hot.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_menu_settings.xml │ │ ├── ic_menu_welfare.xml │ │ ├── ic_message.xml │ │ ├── ic_ranking.xml │ │ ├── ic_search.xml │ │ ├── ic_share.xml │ │ ├── ic_star.xml │ │ ├── ic_star_border.xml │ │ ├── ic_theme.xml │ │ ├── ic_time.xml │ │ ├── icon_back.xml │ │ ├── icon_back_white.xml │ │ ├── no_banner.png │ │ └── wukong.jpeg │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ ├── splash_image01.jpg │ │ ├── splash_image02.jpg │ │ ├── splash_image03.jpg │ │ ├── splash_image04.jpg │ │ └── splash_image05.jpg │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── com │ └── mm │ └── hamcompose │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshot ├── Screenshot_20210927_111453_com.mm.hamcompose.jpg ├── Screenshot_20210927_111526_com.mm.hamcompose.jpg ├── Screenshot_20210927_111602_com.mm.hamcompose.jpg ├── Screenshot_20210927_111619_com.mm.hamcompose.jpg ├── Screenshot_20210927_111628_com.mm.hamcompose.jpg ├── Screenshot_20210927_111722_com.mm.hamcompose.jpg ├── Screenshot_20210927_111736_com.mm.hamcompose.jpg ├── Screenshot_20210927_111821_com.mm.hamcompose.jpg └── Screenshot_20210927_111932_com.mm.hamcompose.jpg └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/kotlinScripting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WanAndroidCompose版本 2 | 3 | #### 介绍 4 | 此WanAndroid app客户端项目使用Android官方的Jetpack Compose完成, 5 | 遵循MVVM架构思路,以下为本项目用到的框架: 6 | jetpack compose, viewModel, retrofit, okhttp3, coroutine/flow, paging3, 7 | room, accompanist, hilt, gson, glide/picasso, navigation. 8 | 9 | 项目模块: 10 | 首页(推荐、广场、项目、问答), 11 | 分类(体系、导航、公众号,分享文章), 12 | 收藏(网址、文章), 13 | 我的(我的文章、积分排行、历史浏览、添加文章、设置、消息、主题色、清缓存等) 14 | 登录、登出、注册 15 | 16 | 17 | #### 软件架构 18 | Mvvm, Composable + viewModel + repository 19 | 20 | #### ScreenShot 21 | https://github.com/manqianzhuang/HamApp/tree/origin/screenshot 22 | 23 | 24 | #### 关于项目 25 | 26 | 1. 项目地址: https://github.com/manqianzhuang/HamApp.git 27 | 2. apk地址: https://www.pgyer.com/F9NX 28 | 3. 联系方式: ganzhuangman@gmail.com 29 | 4. API提供: 鸿洋(WanAndroid开放api) 30 | 31 | #### 使用说明 32 | 33 | 1. 此项目仅提供学习用途,未经允许不得用于商业项目 34 | 2. 感谢鸿洋大佬提供的WanAndroid网站,让我们可以学习到很多的android/flutter/前端等技术 35 | 3. 欢迎各位提PR,我会抽时间不断优化代码和修复bug。如有请教,请邮件联系 36 | 37 | #### TODO 38 | 1. 添加动画 = WAIT 39 | 2. 我的消息开发 = WAIT 40 | 41 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /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/androidTest/java/com/mm/hamcompose/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.mm.hamcompose", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/HamApp.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.content.Context 6 | import com.mm.hamcompose.data.store.DataStoreUtils 7 | import dagger.hilt.android.HiltAndroidApp 8 | 9 | /** 10 | * 1. 所有使用 Hilt 的 App 必须包含 一个使用 @HiltAndroidApp 注解的 Application 11 | * 2. @HiltAndroidApp 将会触发 Hilt 代码的生成,包括用作应用程序依赖项容器的基类 12 | * 3. 生成的 Hilt 组件依附于 Application 的生命周期,它也是 App 的父组件,提供其他组件访问的依赖 13 | * 4. 在 Application 中设置好 @HiltAndroidApp 之后,就可以使用 Hilt 提供的组件了, 14 | * Hilt 提供的 @AndroidEntryPoint 注解用于提供 Android 类的依赖(Activity、Fragment、View、Service、BroadcastReceiver)等等 15 | * Application 使用 @HiltAndroidApp 注解 16 | */ 17 | @HiltAndroidApp 18 | class HamApp: Application() { 19 | companion object { 20 | @SuppressLint("StaticFieldLeak") 21 | lateinit var CONTEXT: Context 22 | } 23 | 24 | override fun onCreate() { 25 | super.onCreate() 26 | CONTEXT = this 27 | DataStoreUtils.init(this) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/bean/ConstBean.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.bean 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.mm.hamcompose.data.db.DbConst 6 | 7 | const val MY_USER_ID = -999 8 | 9 | data class MenuTitle( 10 | val title: String, 11 | val iconRes: Int? 12 | ) 13 | 14 | data class TabTitle( 15 | val id: Int, 16 | val text: String, 17 | var cachePosition: Int = 0, 18 | var selected: Boolean = false 19 | ) 20 | 21 | @Entity(tableName = DbConst.history) 22 | data class HistoryRecord( 23 | @PrimaryKey var id: Int, 24 | var title: String, 25 | var link: String, 26 | var niceDate: String, 27 | var shareUser: String, 28 | var userId: Int, 29 | var author: String, 30 | var superChapterId: Int, 31 | var superChapterName: String, 32 | var chapterId: Int, 33 | var chapterName: String, 34 | var desc: String, 35 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/db/DbConst.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.db 2 | 3 | object DbConst { 4 | const val dbVersion = 1 5 | const val hotKeyDbName = "hot_key_db" 6 | const val userDbName = "user_db" 7 | const val historyDbName = "history_db" 8 | const val hotKey = "hot_key" 9 | const val userInfo = "user_info" 10 | const val history = "history" 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/db/history/HistoryDao.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.db.history 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.mm.hamcompose.data.bean.HistoryRecord 8 | 9 | @Dao 10 | interface HistoryDao { 11 | 12 | @Insert(onConflict = OnConflictStrategy.REPLACE) 13 | suspend fun insertHistory(vararg history: HistoryRecord) 14 | 15 | @Query("SELECT * FROM history") 16 | suspend fun queryAll(): Array 17 | 18 | @Query("DELETE FROM history") 19 | suspend fun deleteAll() 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/db/history/HistoryDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.db.history 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.mm.hamcompose.data.bean.HistoryRecord 6 | import com.mm.hamcompose.data.db.DbConst 7 | 8 | @Database(entities = [HistoryRecord::class], version = DbConst.dbVersion) 9 | abstract class HistoryDatabase: RoomDatabase() { 10 | abstract fun historyDao(): HistoryDao 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/db/hotkey/HotKeyDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.db.hotkey 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.mm.hamcompose.data.bean.Hotkey 6 | import com.mm.hamcompose.data.db.DbConst 7 | 8 | @Database(entities = [Hotkey::class], version = DbConst.dbVersion) 9 | abstract class HotkeyDatabase: RoomDatabase() { 10 | abstract fun hotkeyDao(): HotkeysDao 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/db/hotkey/HotkeysDao.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.db.hotkey 2 | 3 | import androidx.room.* 4 | import com.mm.hamcompose.data.bean.Hotkey 5 | 6 | @Dao 7 | interface HotkeysDao { 8 | 9 | @Insert(onConflict = OnConflictStrategy.REPLACE) 10 | suspend fun insertHotkeys(vararg keys: Hotkey) 11 | 12 | @Update 13 | suspend fun updateHotkeys(vararg keys: Hotkey) 14 | 15 | @Delete 16 | suspend fun deleteKeys(vararg keys: Hotkey) 17 | 18 | @Query("SELECT * FROM hot_key") 19 | suspend fun loadAllKeys(): Array 20 | 21 | @Query("DELETE FROM hot_key") 22 | suspend fun deleteAll() 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/db/user/UserInfoDao.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.db.user 2 | 3 | import androidx.room.* 4 | import com.mm.hamcompose.data.bean.UserInfo 5 | import com.mm.hamcompose.data.db.DbConst 6 | import retrofit2.http.DELETE 7 | 8 | @Dao 9 | interface UserInfoDao { 10 | 11 | @Insert(onConflict = OnConflictStrategy.REPLACE) 12 | suspend fun insertUserInfo(userInfo: UserInfo) 13 | 14 | @Update 15 | suspend fun updateUserInfo(userInfo: UserInfo) 16 | 17 | @Query("SELECT * FROM user_info") 18 | suspend fun queryUserInfo(): List 19 | 20 | @Delete(entity = UserInfo::class) 21 | suspend fun deleteUserInfo(vararg userInfo: UserInfo): Int 22 | 23 | @Query("DELETE FROM user_info") 24 | suspend fun deleteAllUserInfo() 25 | 26 | 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/db/user/UserInfoDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.db.user 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | import com.mm.hamcompose.data.bean.IntTypeConverter 7 | import com.mm.hamcompose.data.bean.UserInfo 8 | import com.mm.hamcompose.data.db.DbConst 9 | 10 | @Database(entities = [UserInfo::class], version = DbConst.dbVersion) 11 | @TypeConverters(IntTypeConverter::class) 12 | abstract class UserInfoDatabase: RoomDatabase() { 13 | abstract fun userInfoDao(): UserInfoDao 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/http/ApiCall.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.http 2 | 3 | import android.annotation.SuppressLint 4 | import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory 5 | import com.mm.hamcompose.data.http.interceptor.CacheCookieInterceptor 6 | import com.mm.hamcompose.data.http.interceptor.LogInterceptor 7 | import com.mm.hamcompose.data.http.interceptor.SetCookieInterceptor 8 | import okhttp3.OkHttpClient 9 | import retrofit2.Retrofit 10 | import retrofit2.converter.gson.GsonConverterFactory 11 | import java.security.SecureRandom 12 | import java.security.cert.X509Certificate 13 | import java.util.concurrent.TimeUnit 14 | import javax.net.ssl.* 15 | 16 | /** 17 | * Created by Superman. 19/5/27 18 | */ 19 | object ApiCall { 20 | 21 | /** 22 | * 请求超时时间 23 | */ 24 | private const val DEFAULT_TIMEOUT = 30000 25 | private lateinit var SERVICE: HttpService 26 | 27 | //手动创建一个OkHttpClient并设置超时时间 28 | val retrofit: HttpService 29 | get() { 30 | if (!ApiCall::SERVICE.isInitialized) { 31 | SERVICE = Retrofit.Builder() 32 | .client(okHttp) 33 | .addConverterFactory(GsonConverterFactory.create()) 34 | .addCallAdapterFactory(CoroutineCallAdapterFactory.invoke()) 35 | .baseUrl(HttpService.url) 36 | .build() 37 | .create(HttpService::class.java) 38 | } 39 | return SERVICE 40 | } 41 | 42 | //手动创建一个OkHttpClient并设置超时时间 43 | val okHttp: OkHttpClient 44 | get() { 45 | return OkHttpClient.Builder().run { 46 | connectTimeout(DEFAULT_TIMEOUT.toLong(), TimeUnit.MILLISECONDS) 47 | readTimeout(DEFAULT_TIMEOUT.toLong(), TimeUnit.MILLISECONDS) 48 | writeTimeout(DEFAULT_TIMEOUT.toLong(), TimeUnit.MILLISECONDS) 49 | addInterceptor(SetCookieInterceptor()) 50 | addInterceptor(CacheCookieInterceptor()) 51 | addInterceptor(LogInterceptor()) 52 | //不验证证书 53 | sslSocketFactory(createSSLSocketFactory()) 54 | hostnameVerifier(TrustAllNameVerifier()) 55 | build() 56 | } 57 | } 58 | 59 | private fun createSSLSocketFactory(): SSLSocketFactory { 60 | lateinit var ssfFactory: SSLSocketFactory 61 | try { 62 | val sslFactory = SSLContext.getInstance("TLS") 63 | sslFactory.init(null, arrayOf(TrustAllCerts()), SecureRandom()); 64 | ssfFactory = sslFactory.socketFactory 65 | } catch (e: Exception) { 66 | print("SSL错误:${e.message}") 67 | } 68 | return ssfFactory 69 | } 70 | 71 | } 72 | 73 | class TrustAllNameVerifier: HostnameVerifier { 74 | @SuppressLint("BadHostnameVerifier") 75 | override fun verify(hostname: String?, session: SSLSession?): Boolean = true 76 | } 77 | 78 | @SuppressLint("CustomX509TrustManager") 79 | class TrustAllCerts : X509TrustManager { 80 | 81 | @SuppressLint("TrustAllX509TrustManager") 82 | override fun checkClientTrusted(chain: Array?, authType: String?) {} 83 | 84 | @SuppressLint("TrustAllX509TrustManager") 85 | override fun checkServerTrusted(chain: Array?, authType: String?) {} 86 | 87 | override fun getAcceptedIssuers(): Array = arrayOf() 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/http/HttpResult.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.http 2 | 3 | import java.lang.Exception 4 | 5 | sealed class HttpResult { 6 | 7 | data class Success(val result: T): HttpResult() 8 | data class Error(val exception: Exception): HttpResult() 9 | 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/http/interceptor/CacheCookieInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.http.interceptor 2 | 3 | import com.mm.hamcompose.data.store.DataStoreUtils 4 | import okhttp3.Interceptor 5 | import okhttp3.Response 6 | 7 | class CacheCookieInterceptor: Interceptor { 8 | 9 | private val loginUrl = "user/login" 10 | private val registerUrl = "user/register" 11 | private val SET_COOKIE_KEY = "set-cookie" 12 | 13 | override fun intercept(chain: Interceptor.Chain): Response { 14 | val request = chain.request() 15 | val response = chain.proceed(request) 16 | val requestUrl = request.url().toString() 17 | val domain = request.url().host() 18 | if (aboutUser(requestUrl)) { 19 | val cookies = response.headers(SET_COOKIE_KEY) 20 | if (cookies.isNotEmpty()) { 21 | //cookie可能有多个,都保存下来 22 | DataStoreUtils.putSyncData(domain, encodeCookie(cookies)) 23 | } 24 | } 25 | return response 26 | } 27 | 28 | private fun aboutUser(url: String): Boolean = url.contains(loginUrl) or url.contains(registerUrl) 29 | } 30 | 31 | /** 32 | * 整理cookie 33 | */ 34 | private fun encodeCookie(cookies: List): String { 35 | val sb = StringBuilder() 36 | val set = HashSet() 37 | cookies 38 | .map { cookie -> 39 | cookie.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() 40 | } 41 | .forEach { it -> 42 | it.filterNot { set.contains(it) }.forEach { set.add(it) } 43 | } 44 | 45 | val ite = set.iterator() 46 | while (ite.hasNext()) { 47 | val cookie = ite.next() 48 | sb.append(cookie).append(";") 49 | } 50 | 51 | val last = sb.lastIndexOf(";") 52 | if (sb.length - 1 == last) { 53 | sb.deleteCharAt(last) 54 | } 55 | 56 | return sb.toString() 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/http/interceptor/SetCookieInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.http.interceptor 2 | 3 | import com.mm.hamcompose.data.store.DataStoreUtils 4 | import okhttp3.Interceptor 5 | import okhttp3.Response 6 | 7 | class SetCookieInterceptor: Interceptor { 8 | 9 | override fun intercept(chain: Interceptor.Chain): Response { 10 | val request = chain.request() 11 | val builder = request.newBuilder() 12 | val domain = request.url().host() 13 | //获取domain内的cookie 14 | if (domain.isNotEmpty()) { 15 | val cookie: String = DataStoreUtils.readStringData(domain, "") 16 | if (cookie.isNotEmpty()) { 17 | builder.addHeader("Cookie", cookie) 18 | } 19 | } 20 | return chain.proceed(builder.build()) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/http/paging/BasePagingSource.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.http.paging 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.blankj.utilcode.util.LogUtils 6 | import com.mm.hamcompose.data.bean.BasicBean 7 | import com.mm.hamcompose.data.bean.ListWrapper 8 | import com.mm.hamcompose.data.http.HttpResult 9 | 10 | class BasePagingSource constructor( 11 | private val callDataFromRemoteServer: suspend (page: Int)-> HttpResult>> 12 | ): PagingSource() { 13 | 14 | private var page: Int = -1 15 | 16 | override fun getRefreshKey(state: PagingState): Int? { 17 | return state.anchorPosition?.let { 18 | val anchorPage = state.closestPageToPosition(it) 19 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) 20 | } 21 | } 22 | 23 | override suspend fun load(params: LoadParams): LoadResult { 24 | println("当前页 ${params.key}") 25 | page = params.key ?: 0 26 | return when (val response = callDataFromRemoteServer(page)) { 27 | is HttpResult.Success -> { 28 | val data = response.result.data 29 | val hasNotNext = (data!!.datas.size < params.loadSize) && (data.over) 30 | LoadResult.Page( 31 | data = response.result.data!!.datas, 32 | prevKey = if (page - 1 > 0) page - 1 else null, 33 | nextKey = if (hasNotNext) null else page+1 34 | ) 35 | } 36 | is HttpResult.Error -> { 37 | LogUtils.e("网络请求异常: ${response.exception.message}") 38 | LoadResult.Error(response.exception) 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/http/paging/GirlPhotoPagingSource.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.http.paging 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.blankj.utilcode.util.LogUtils 6 | import com.mm.hamcompose.data.bean.WelfareData 7 | import com.mm.hamcompose.data.http.HttpService 8 | import javax.inject.Inject 9 | 10 | class GirlPhotoPagingSource @Inject constructor( 11 | private val apiService: HttpService, 12 | ): PagingSource() { 13 | 14 | override fun getRefreshKey(state: PagingState): Int? = null 15 | 16 | override suspend fun load(params: LoadParams): LoadResult { 17 | return try { 18 | LogUtils.e("currentPage= ${params.key}, size=${params.loadSize}") 19 | val page = params.key?: 0 20 | val response = apiService.getWelfareList("Girl", "Girl", page, params.loadSize) 21 | val isNextPage = response.data!!.isNotEmpty() 22 | LoadResult.Page( 23 | data = response.data!!, 24 | prevKey = if (page>0) page-1 else null, 25 | nextKey = if (isNextPage) page+1 else null 26 | ) 27 | } catch (e: Exception) { 28 | LogUtils.e("网络请求异常: ${e.message}") 29 | LoadResult.Error(e) 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/data/http/paging/PagingFactory.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.data.http.paging 2 | 3 | import androidx.paging.PagingConfig 4 | 5 | class PagingFactory { 6 | 7 | val pagingConfig = PagingConfig( 8 | 9 | // 每页显示的数据的大小 10 | pageSize = 20, 11 | //开启占位符 12 | enablePlaceholders = true, 13 | //预刷新的距离,距离最后一个 item 多远时加载数据 14 | prefetchDistance = 4, 15 | //初始化加载数量,默认为 pageSize * 3 16 | initialLoadSize = 20 17 | ) 18 | 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/di/module/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.di.module 2 | 3 | import androidx.room.Room 4 | import com.mm.hamcompose.HamApp 5 | import com.mm.hamcompose.data.db.DbConst 6 | import com.mm.hamcompose.data.db.history.HistoryDatabase 7 | import com.mm.hamcompose.data.db.hotkey.HotkeyDatabase 8 | import com.mm.hamcompose.data.db.user.UserInfoDatabase 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.components.SingletonComponent 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | class DatabaseModule { 18 | 19 | @Singleton 20 | @Provides 21 | fun provideHotkeyDataBase(): HotkeyDatabase { 22 | return Room.databaseBuilder(HamApp.CONTEXT, HotkeyDatabase::class.java, DbConst.hotKeyDbName) 23 | .build() 24 | } 25 | 26 | @Singleton 27 | @Provides 28 | fun provideUserInfoDataBase(): UserInfoDatabase { 29 | return Room.databaseBuilder(HamApp.CONTEXT, UserInfoDatabase::class.java, DbConst.userDbName) 30 | .build() 31 | } 32 | 33 | @Singleton 34 | @Provides 35 | fun provideHistoryDataBase(): HistoryDatabase { 36 | return Room.databaseBuilder(HamApp.CONTEXT, HistoryDatabase::class.java, DbConst.historyDbName) 37 | .build() 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/di/module/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.di.module 2 | 3 | import com.mm.hamcompose.data.http.ApiCall 4 | import com.mm.hamcompose.data.http.HttpService 5 | import com.mm.hamcompose.data.http.interceptor.LogInterceptor 6 | import com.mm.hamcompose.repository.HttpRepository 7 | import com.mm.hamcompose.repository.HttpRepositoryImpl 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.components.SingletonComponent 12 | import okhttp3.Interceptor 13 | import okhttp3.OkHttpClient 14 | import javax.inject.Singleton 15 | 16 | //这里使用了SingletonComponent,因此 NetworkModule 绑定到 Application 的生命周期 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | class NetworkModule { 20 | 21 | @Singleton 22 | @Provides 23 | fun provideApiService(): HttpService = ApiCall.retrofit 24 | 25 | @Singleton 26 | @Provides 27 | fun provideOkHttp(): OkHttpClient = ApiCall.okHttp 28 | 29 | @Singleton 30 | @Provides 31 | fun provideLogInterceptor(): Interceptor = LogInterceptor() 32 | 33 | @Provides 34 | fun provideRepository(apiService: HttpService): HttpRepository { 35 | return HttpRepositoryImpl(apiService) 36 | } 37 | 38 | // @Singleton 39 | // @Provides 40 | // fun provideRepo(apiService: HttpService): HttpRepository { 41 | // return HttpRepository(apiService) 42 | // } 43 | 44 | 45 | 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/di/scope/ActivityScope.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.di.scope 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @MustBeDocumented 7 | annotation class ActivityScope() 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/di/scope/ApplicationScope.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.di.scope 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @MustBeDocumented 7 | annotation class ApplicationScope() 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/di/scope/FragmentScope.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.di.scope 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @MustBeDocumented 7 | annotation class FragmentScope() 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/repository/HttpRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.repository 2 | 3 | import androidx.paging.PagingData 4 | import com.mm.hamcompose.data.bean.* 5 | import com.mm.hamcompose.data.http.HttpResult 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | //类型别名,用于定义较长的泛型类型 9 | typealias BANNER = Flow>> 10 | typealias ARTICLE = Flow>> 11 | typealias HOTKEY = Flow>> 12 | typealias PARENT = Flow>> 13 | typealias NAVIGATION = Flow>> 14 | typealias USERINFO = Flow> 15 | typealias POINTS = Flow> 16 | typealias ANY = Flow> 17 | typealias COLLECT = Flow> 18 | typealias SHARER = Flow>> 19 | typealias ONE_PARENT = Flow> 20 | typealias BASIC_USERINFO = Flow> 21 | 22 | typealias WELFARE = Flow> 23 | 24 | typealias PagingAny = Flow> 25 | typealias PagingPoints = Flow> 26 | typealias PagingCollect = Flow> 27 | typealias PagingArticle = Flow> 28 | typealias PagingWelfare = Flow> 29 | 30 | interface HttpRepository { 31 | //普通请求 32 | suspend fun getBanners(): BANNER 33 | suspend fun getTopArticles(): ARTICLE 34 | suspend fun getHotkeys(): HOTKEY 35 | suspend fun getStructureList(): PARENT 36 | suspend fun getNavigationList(): NAVIGATION 37 | suspend fun getPublicInformation(): PARENT 38 | suspend fun getProjectCategory(): PARENT 39 | suspend fun register(userName: String, password: String, repassword: String): USERINFO 40 | suspend fun login(userName: String, password: String): USERINFO 41 | suspend fun logout(): ANY 42 | suspend fun getMyPointsRanking(): POINTS 43 | suspend fun getMessageCount(): Flow> 44 | suspend fun getCollectUrls(): PARENT 45 | suspend fun collectInnerArticle(id: Int): ANY 46 | suspend fun uncollectInnerArticle(id: Int): ANY 47 | suspend fun uncollectArticleById(id: Int, originId: Int): ANY 48 | suspend fun addNewWebsiteCollect(title: String, linkUrl: String): ONE_PARENT 49 | suspend fun addNewArticleCollect(title: String, linkUrl: String, author: String): COLLECT 50 | suspend fun deleteWebsite(id: Int): ANY 51 | suspend fun editCollectWebsite(id: Int, title: String, linkUrl: String): ANY 52 | suspend fun getMyShareArticles(page: Int): SHARER 53 | suspend fun getAuthorShareArticles(userId: Int, page: Int): SHARER 54 | suspend fun deleteMyShareArticle(articleId: Int): ANY 55 | suspend fun addMyShareArticle(title: String, link: String, shareUser: String): ANY 56 | suspend fun getBasicUserInfo(): BASIC_USERINFO 57 | 58 | //干货 gank.io的妹纸福利列表 59 | suspend fun getWelfareData(page: Int, pageSize: Int): WELFARE 60 | 61 | //分页请求 62 | fun getIndexData(): PagingArticle 63 | fun getSquareData(): PagingArticle 64 | fun getWendaData(): PagingArticle 65 | fun getProjects(cId: Int): PagingArticle 66 | fun getPublicArticles(publicId: Int): PagingArticle 67 | fun getStructureArticles(param: Any): PagingArticle 68 | fun searchArticleWithKey(publicId: Int, key: String): PagingArticle 69 | fun queryArticle(key: String): PagingArticle 70 | fun getPointsRankings(): PagingPoints 71 | fun getPointsRecords(): PagingPoints 72 | fun getCollectionList(): PagingCollect 73 | fun getUnreadMessages(): PagingAny 74 | fun getReadedMessages(): PagingAny 75 | fun getWelfareData(key: String): PagingWelfare 76 | 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.mm.hamcompose.HamApp 5 | import com.mm.hamcompose.R 6 | 7 | val Transparent = Color(0x00000000) 8 | 9 | val themeColors = arrayOf( 10 | Color(HamApp.CONTEXT.resources.getColor(R.color.primary)), 11 | Color(HamApp.CONTEXT.resources.getColor(R.color.purple_200)), 12 | Color(HamApp.CONTEXT.resources.getColor(R.color.purple_500)), 13 | Color(HamApp.CONTEXT.resources.getColor(R.color.purple_700)), 14 | Color(HamApp.CONTEXT.resources.getColor(R.color.teal_700)), 15 | Color(HamApp.CONTEXT.resources.getColor(R.color.navajo_white)), 16 | Color(HamApp.CONTEXT.resources.getColor(R.color.medium_blue)), 17 | Color(HamApp.CONTEXT.resources.getColor(R.color.hot_pink)), 18 | Color(HamApp.CONTEXT.resources.getColor(R.color.chocolate)), 19 | Color(HamApp.CONTEXT.resources.getColor(R.color.dark_orange)), 20 | Color(HamApp.CONTEXT.resources.getColor(R.color.orange)), 21 | Color(HamApp.CONTEXT.resources.getColor(R.color.gold)), 22 | Color(HamApp.CONTEXT.resources.getColor(R.color.yellow)), 23 | Color(HamApp.CONTEXT.resources.getColor(R.color.fire_red)), 24 | Color(HamApp.CONTEXT.resources.getColor(R.color.light_green)), 25 | Color(HamApp.CONTEXT.resources.getColor(R.color.sprint_green)), 26 | //Color(HamApp.CONTEXT.resources.getColor(R.color.azure)), 27 | ) 28 | 29 | val splashText = Color(0x25000000) 30 | val white = Color(0xFFFFFFFF) 31 | val white1 = Color(0xFFF7F7F7) 32 | val white2 = Color(0xFFEDEDED) 33 | val white3 = Color(0xFFE5E5E5) 34 | val white4 = Color(0xFFD5D5D5) 35 | val white5 = Color(0xFFCCCCCC) 36 | val black = Color(0xFF000000) 37 | val black1 = Color(0xFF1E1E1E) 38 | val black2 = Color(0xFF111111) 39 | val black3 = Color(0xFF191919) 40 | val black4 = Color(0xFF252525) 41 | val black5 = Color(0xFF2C2C2C) 42 | val black6 = Color(0xFF07130A) 43 | val black7 = Color(0xFF292929) 44 | val grey1 = Color(0xFF888888) 45 | val grey2 = Color(0xFFCCC7BF) 46 | val grey3 = Color(0xFF767676) 47 | val grey4 = Color(0xFFB2B2B2) 48 | val grey5 = Color(0xFF5E5E5E) 49 | val green1 = Color(0xFFB0EB6E) 50 | val green2 = Color(0xFF6DB476) 51 | val green3 = Color(0xFF67BF63) 52 | val red = Color(0xFFFF0000) 53 | val red1 = Color(0xFFDF5554) 54 | val red2 = Color(0xFFDD302E) 55 | val red3 = Color(0xFFF77B7A) 56 | val red4 = Color(0xFFD42220) 57 | val red5 = Color(0xFFC51614) 58 | val red6 = Color(0xFFF74D4B) 59 | val red7 = Color(0xFFDC514E) 60 | val red8 = Color(0xFFCBC7BF) 61 | val yellow1 = Color(0xFFF6CA23) 62 | val blue = Color(0xFF0000FF) 63 | val info = Color(0xFF018786) 64 | val warn = Color(0xFFD87831) -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/theme/Dimens.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.theme 2 | 3 | import androidx.compose.ui.unit.dp 4 | import androidx.compose.ui.unit.sp 5 | 6 | val ToolBarHeight = 48.dp 7 | val TabBarHeight = 48.dp 8 | val SearchBarHeight = 42.dp 9 | val BottomNavBarHeight = 56.dp 10 | val ListTitleHeight = 30.dp 11 | 12 | val PrimaryButtonHeight = 36.dp 13 | val MediumButtonHeight = 28.dp 14 | val SmallButtonHeight = 28.dp 15 | 16 | 17 | val H1 = 48.sp //超大号标题 18 | val H2 = 36.sp //大号标题 19 | val H3 = 24.sp //主标题 20 | val H4 = 20.sp //普通标题 21 | val H5 = 16.sp //内容文本 22 | val H6 = 14.sp //普通文字尺寸 23 | val H7 = 12.sp //提示语尺寸 24 | 25 | val ToolBarTitleSize = 18.sp 26 | 27 | val cardCorner = 5.dp //卡片的圆角 28 | val buttonCorner = 3.dp //按钮的圆角 29 | val buttonHeight = 36.dp //按钮的高度 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val HamShapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.theme 2 | 3 | import androidx.compose.material.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 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/HomeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.core.view.WindowCompat 7 | import com.blankj.utilcode.util.LogUtils 8 | import com.blankj.utilcode.util.ToastUtils 9 | import com.mm.hamcompose.R 10 | import dagger.hilt.android.AndroidEntryPoint 11 | import kotlin.system.exitProcess 12 | 13 | @AndroidEntryPoint 14 | class HomeActivity : ComponentActivity() { 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | 19 | WindowCompat.setDecorFitsSystemWindows(window, false) 20 | window.navigationBarColor = resources.getColor(R.color.transparent) 21 | // window.setFlags( 22 | // WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, 23 | // WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION 24 | // ) 25 | setContent { HomeEntry(onBackPressedDispatcher) } 26 | } 27 | 28 | private var cacheMills: Long = 0L 29 | override fun onBackPressed() { 30 | LogUtils.e("是否可以回退 ${onBackPressedDispatcher.hasEnabledCallbacks()}") 31 | if (!onBackPressedDispatcher.hasEnabledCallbacks()) { 32 | if (System.currentTimeMillis() - cacheMills > 1000L) { 33 | cacheMills = System.currentTimeMillis() 34 | ToastUtils.showShort("连按两次退出app") 35 | } else { 36 | this.finish() 37 | exitProcess(0) 38 | } 39 | } 40 | else super.onBackPressed() 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/HomeEntry.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui 2 | 3 | import androidx.activity.OnBackPressedDispatcher 4 | import androidx.compose.runtime.* 5 | import com.mm.hamcompose.theme.HamTheme 6 | import com.mm.hamcompose.ui.page.base.HamScaffold 7 | import com.mm.hamcompose.ui.page.splash.SplashPage 8 | 9 | @Composable 10 | fun HomeEntry(backDispatcher: OnBackPressedDispatcher) { 11 | 12 | //是否闪屏页 13 | var isSplash by remember { mutableStateOf(true) } 14 | if (isSplash) { 15 | SplashPage { isSplash = false } 16 | } else { 17 | HamTheme { HamScaffold() } 18 | } 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/base/BaseCollectViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.base 2 | 3 | import com.mm.hamcompose.data.http.HttpResult 4 | import com.mm.hamcompose.repository.HttpRepository 5 | import kotlinx.coroutines.flow.collectLatest 6 | 7 | abstract class BaseCollectViewModel constructor( 8 | private val httpRepo: HttpRepository 9 | ) : CacheHistoryViewModel() { 10 | 11 | fun collectArticleById(id: Int) { 12 | async { 13 | httpRepo.collectInnerArticle(id).collectLatest { response -> 14 | when (response) { 15 | is HttpResult.Success -> { } 16 | is HttpResult.Error -> { 17 | //收藏接口,不走success判断分支 18 | val nullNotice = "the result of remote's request is null" 19 | if (response.exception.message==nullNotice) { 20 | println("收藏成功(id=$id)") 21 | message.value = "收藏成功" 22 | } else { 23 | message.value = response.exception.message ?: "未知异常" 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | fun uncollectArticleById(id: Int) { 32 | async { 33 | httpRepo.uncollectInnerArticle(id).collectLatest { response -> 34 | when (response) { 35 | is HttpResult.Success -> { } 36 | is HttpResult.Error -> { 37 | //收藏接口,不走success判断分支 38 | val nullNotice = "the result of remote's request is null" 39 | if (response.exception.message==nullNotice) { 40 | println("取消收藏(id=$id)") 41 | message.value = "取消收藏" 42 | } else { 43 | message.value = response.exception.message ?: "未知异常" 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/base/BaseRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.base 2 | 3 | import androidx.paging.Pager 4 | import androidx.paging.PagingConfig 5 | import androidx.paging.PagingData 6 | import com.mm.hamcompose.data.bean.BasicBean 7 | import com.mm.hamcompose.data.bean.ListWrapper 8 | import com.mm.hamcompose.data.http.HttpResult 9 | import com.mm.hamcompose.data.http.paging.BasePagingSource 10 | import com.mm.hamcompose.data.http.paging.PagingFactory 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.flow 14 | import kotlinx.coroutines.flow.flowOn 15 | 16 | open class BaseRepository { 17 | 18 | fun flowable(call: suspend ()-> BasicBean): Flow> { 19 | return flow { 20 | val result = try { 21 | val response = call() 22 | if (response.errorCode==0) { 23 | if (response.data!=null) { 24 | HttpResult.Success(response.data!!) 25 | } else { 26 | throw Exception("the result of remote's request is null") 27 | } 28 | } else { 29 | throw Exception(response.errorMsg) 30 | } 31 | } catch (ex: Exception) { 32 | HttpResult.Error(ex) 33 | } 34 | emit(result) 35 | }.flowOn(Dispatchers.IO) 36 | } 37 | 38 | fun pager( 39 | initKey: Int = 0, 40 | baseConfig: PagingConfig = PagingFactory().pagingConfig, 41 | callAction: suspend (page: Int)-> BasicBean> 42 | ): Flow> { 43 | 44 | // config = 加载分页数据的配置项 45 | // initialKey = 设置默认的初始页 46 | // pagingSourceFactory = 加载分页的驱动器 47 | return Pager( 48 | config = baseConfig, 49 | initialKey = initKey, 50 | pagingSourceFactory = { 51 | BasePagingSource { 52 | try { 53 | HttpResult.Success(callAction(it)) 54 | } catch (e: Exception) { 55 | HttpResult.Error(e) 56 | } 57 | } 58 | }).flow 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.base 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import kotlinx.coroutines.launch 7 | 8 | abstract class BaseViewModel : ViewModel() { 9 | 10 | //分类列表(装非分页加载的容器) 11 | var list = mutableStateOf(mutableListOf()) 12 | 13 | var currentListIndex = mutableStateOf(0) 14 | 15 | var loading = mutableStateOf(false) 16 | 17 | private var _isInited = mutableStateOf(false) 18 | 19 | var message = mutableStateOf("") 20 | 21 | private val isInited: Boolean 22 | get() = _isInited.value 23 | 24 | private fun requestInitialized() { 25 | _isInited.value = true 26 | } 27 | 28 | fun resetListIndex() { 29 | currentListIndex.value = 0 30 | } 31 | 32 | fun resetInitState() { 33 | _isInited.value = false 34 | } 35 | 36 | fun async(block: suspend ()-> Unit) { 37 | viewModelScope.launch { block() } 38 | } 39 | 40 | abstract fun start() 41 | 42 | fun initThat(block: () -> Unit) { 43 | if (!isInited) { 44 | block.invoke() 45 | requestInitialized() 46 | } 47 | } 48 | 49 | fun savePosition(index: Int) { 50 | currentListIndex.value = index 51 | println("## save position = $index ##") 52 | } 53 | 54 | fun stopLoading() { 55 | loading.value = false 56 | } 57 | 58 | fun startLoading() { 59 | loading.value = true 60 | } 61 | 62 | open fun loadContent() { } 63 | 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/base/CacheHistoryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.base 2 | 3 | import com.mm.hamcompose.data.bean.Article 4 | import com.mm.hamcompose.data.bean.HistoryRecord 5 | import com.mm.hamcompose.data.db.history.HistoryDatabase 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | 9 | abstract class CacheHistoryViewModel: BaseViewModel() { 10 | 11 | fun cacheHistory(db: HistoryDatabase, article: Article) { 12 | async { 13 | val history = toMapData(article) 14 | withContext(Dispatchers.IO) { 15 | db.historyDao().insertHistory(history) 16 | println("成功储存到历史记录") 17 | } 18 | } 19 | } 20 | 21 | private fun toMapData(article: Article): HistoryRecord { 22 | return with(article) { 23 | HistoryRecord( 24 | id = id, 25 | title = title ?: "", 26 | link = link ?: "", 27 | niceDate = niceDate ?: "", 28 | shareUser = shareUser ?: "", 29 | userId = userId, 30 | author = author ?: "", 31 | superChapterId = superChapterId, 32 | superChapterName = superChapterName ?: "", 33 | chapterId = chapterId, 34 | chapterName = chapterName ?: "", 35 | desc = desc ?: "" 36 | ) 37 | } 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/girls/info/GirlInfoPage.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.girls.info 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.material.ScaffoldState 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Brush 11 | import androidx.compose.ui.layout.ContentScale 12 | import androidx.compose.ui.unit.dp 13 | import androidx.navigation.NavHostController 14 | import coil.compose.rememberImagePainter 15 | import com.google.accompanist.pager.ExperimentalPagerApi 16 | import com.mm.hamcompose.R 17 | import com.mm.hamcompose.data.bean.WelfareData 18 | import com.mm.hamcompose.theme.HamTheme 19 | import com.mm.hamcompose.ui.route.RouteUtils.back 20 | import com.mm.hamcompose.ui.widget.HamToolBar 21 | import com.mm.hamcompose.ui.widget.MainTitle 22 | import com.mm.hamcompose.ui.widget.TextContent 23 | 24 | @OptIn(ExperimentalPagerApi::class) 25 | @Composable 26 | fun GirlInfoPage( 27 | welfare: WelfareData, 28 | navCtrl: NavHostController, 29 | scaffoldState: ScaffoldState, 30 | ) { 31 | Column { 32 | HamToolBar(title = welfare.title!!, onBack = { navCtrl.back() }) 33 | PhotoView(welfare = welfare) 34 | } 35 | } 36 | 37 | @Composable 38 | private fun PhotoView(welfare: WelfareData) { 39 | Box { 40 | Image( 41 | painter = rememberImagePainter( 42 | data = welfare.url, 43 | builder = { 44 | crossfade(true) 45 | placeholder(R.drawable.no_banner) 46 | 47 | }, 48 | ), 49 | contentDescription = welfare.author, 50 | contentScale = ContentScale.FillHeight, 51 | modifier = Modifier.fillMaxSize() 52 | ) 53 | MainTitle( 54 | title = welfare.author!!, 55 | modifier = Modifier 56 | .padding(10.dp) 57 | .align(Alignment.TopStart) 58 | ) 59 | TextContent( 60 | text = welfare.desc!!, 61 | modifier = Modifier 62 | .padding(10.dp) 63 | .wrapContentSize() 64 | .align(Alignment.BottomCenter) 65 | .background( 66 | brush = Brush.horizontalGradient( 67 | listOf(HamTheme.colors.placeholder, HamTheme.colors.placeholder) 68 | ), 69 | alpha = 0.3f 70 | ) 71 | .padding(horizontal = 10.dp) 72 | , 73 | color = HamTheme.colors.mainColor 74 | ) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/girls/list/GirlPhotoViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.girls.list 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import com.mm.hamcompose.data.bean.WelfareData 5 | import com.mm.hamcompose.data.http.HttpResult 6 | import com.mm.hamcompose.repository.HttpRepository 7 | import com.mm.hamcompose.ui.page.base.BaseViewModel 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.collectLatest 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class GirlPhotoViewModel @Inject constructor( 14 | private var repo: HttpRepository 15 | ) : BaseViewModel() { 16 | 17 | val pageSize = 40 18 | var page = mutableStateOf(1) 19 | var hasNext = mutableStateOf(false) 20 | 21 | val photoData = mutableStateOf(mutableListOf()) 22 | 23 | override fun start() { 24 | initThat { loadContent() } 25 | } 26 | 27 | fun loadMore() { 28 | if (hasNext.value) { 29 | page.value += 1 30 | loadContent() 31 | } 32 | } 33 | 34 | override fun loadContent() { 35 | async { 36 | repo.getWelfareData(page.value, pageSize).collectLatest { response -> 37 | when (response) { 38 | is HttpResult.Success -> { 39 | val photos = response.result.data 40 | if (!photos.isNullOrEmpty()) { 41 | hasNext.value = true 42 | if (photoData.value.isEmpty()) { 43 | photoData.value = photos as MutableList 44 | } else { 45 | photoData.value.addAll(photos) 46 | } 47 | } else { 48 | hasNext.value = false 49 | } 50 | } 51 | is HttpResult.Error -> { 52 | println(response.exception.message) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/category/CategoryPage.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.category 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.rememberCoroutineScope 11 | import androidx.compose.ui.Modifier 12 | import androidx.hilt.navigation.compose.hiltViewModel 13 | import androidx.navigation.NavHostController 14 | import com.google.accompanist.pager.ExperimentalPagerApi 15 | import com.google.accompanist.pager.HorizontalPager 16 | import com.google.accompanist.pager.rememberPagerState 17 | import com.mm.hamcompose.theme.BottomNavBarHeight 18 | import com.mm.hamcompose.ui.page.main.category.navigation.NaviPage 19 | import com.mm.hamcompose.ui.page.main.category.pubaccount.category.PublicAccountPage 20 | import com.mm.hamcompose.ui.page.main.category.structure.tree.StructurePage 21 | import com.mm.hamcompose.ui.route.RouteName 22 | import com.mm.hamcompose.ui.route.RouteUtils 23 | import com.mm.hamcompose.ui.widget.TextTabBar 24 | import kotlinx.coroutines.launch 25 | 26 | @OptIn(ExperimentalPagerApi::class) 27 | @Composable 28 | fun CategoryPage( 29 | navCtrl: NavHostController, 30 | categoryIndex: Int = 0, 31 | viewModel: CategoryViewModel = hiltViewModel(), 32 | onPageSelected: (position: Int) -> Unit, 33 | ) { 34 | 35 | val titles by remember { viewModel.titles } 36 | Box(modifier = Modifier.padding(bottom = BottomNavBarHeight)) { 37 | Column { 38 | val pagerState = rememberPagerState( 39 | pageCount = titles.size, 40 | initialPage = categoryIndex, 41 | initialOffscreenLimit = titles.size 42 | ) 43 | val scopeState = rememberCoroutineScope() 44 | 45 | Row { 46 | TextTabBar( 47 | index = pagerState.currentPage, 48 | tabTexts = titles, 49 | modifier = Modifier.weight(1f), 50 | onTabSelected = { index -> 51 | scopeState.launch { 52 | pagerState.scrollToPage(index) 53 | } 54 | }, 55 | withAdd = true, 56 | onAddClick = { 57 | RouteUtils.navTo(navCtrl, RouteName.SHARE_ARTICLE) 58 | } 59 | ) 60 | } 61 | 62 | HorizontalPager(state = pagerState) { page -> 63 | onPageSelected(pagerState.currentPage) 64 | when (page) { 65 | 0 -> StructurePage(navCtrl) 66 | 1 -> NaviPage(navCtrl) 67 | 2 -> PublicAccountPage(navCtrl) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/category/CategoryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.category 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import com.mm.hamcompose.data.bean.ParentBean 5 | import com.mm.hamcompose.data.bean.TabTitle 6 | import com.mm.hamcompose.ui.page.base.BaseViewModel 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import javax.inject.Inject 9 | 10 | @HiltViewModel 11 | class CategoryViewModel @Inject constructor() : BaseViewModel() { 12 | 13 | val titles = mutableStateOf( 14 | mutableListOf( 15 | TabTitle(201, "体系"), 16 | TabTitle(202, "导航"), 17 | TabTitle(203, "公众号"), 18 | ) 19 | ) 20 | 21 | 22 | override fun start() { 23 | 24 | } 25 | 26 | 27 | override fun onCleared() { 28 | super.onCleared() 29 | println("CategoryViewModel ==> onClear") 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/category/navigation/NaviPage.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.category.navigation 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.lazy.itemsIndexed 8 | import androidx.compose.foundation.lazy.rememberLazyListState 9 | import androidx.compose.material.Divider 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.unit.dp 15 | import androidx.hilt.navigation.compose.hiltViewModel 16 | import androidx.navigation.NavHostController 17 | import com.google.accompanist.flowlayout.FlowRow 18 | import com.mm.hamcompose.data.bean.NaviWrapper 19 | import com.mm.hamcompose.data.bean.WebData 20 | import com.mm.hamcompose.theme.HamTheme 21 | import com.mm.hamcompose.ui.route.RouteUtils 22 | import com.mm.hamcompose.ui.route.RouteName 23 | 24 | import com.mm.hamcompose.ui.widget.LabelTextButton 25 | import com.mm.hamcompose.ui.widget.ListTitle 26 | 27 | @OptIn(ExperimentalFoundationApi::class) 28 | @Composable 29 | fun NaviPage( 30 | navCtrl: NavHostController, 31 | viewModel: NaviViewModel = hiltViewModel() 32 | ) { 33 | viewModel.start() 34 | val naviData by remember { viewModel.list } 35 | val isLoading by remember { viewModel.loading } 36 | val currentPosition by remember { viewModel.currentListIndex } 37 | val listState = rememberLazyListState(currentPosition) 38 | 39 | LazyColumn( 40 | modifier = Modifier.fillMaxSize(), 41 | state = listState, 42 | contentPadding = PaddingValues(vertical = 10.dp) 43 | ) { 44 | if (isLoading) { 45 | items(6) { 46 | NaviItem( 47 | wrapper = NaviWrapper(null, -1, ""), 48 | isLoading = isLoading, 49 | ) 50 | } 51 | } else { 52 | naviData.forEachIndexed { index, naviBean -> 53 | stickyHeader { ListTitle(title = naviBean.name ?: "标题") } 54 | item { 55 | NaviItem(naviBean, onSelected = { 56 | viewModel.savePosition(listState.firstVisibleItemIndex) 57 | RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, it) 58 | }) 59 | if (index <= naviData.size - 1) { 60 | Divider( 61 | startIndent = 10.dp, 62 | color = HamTheme.colors.divider, 63 | thickness = 0.8f.dp 64 | ) 65 | } 66 | Spacer(modifier = Modifier.height(10.dp)) 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | @Composable 74 | fun NaviItem( 75 | wrapper: NaviWrapper, 76 | isLoading: Boolean = false, 77 | onSelected: (WebData) -> Unit = {} 78 | ) { 79 | Column( 80 | modifier = Modifier 81 | .fillMaxWidth() 82 | .padding(horizontal = 10.dp) 83 | ) { 84 | if (isLoading) { 85 | ListTitle(title = "我是标题") 86 | FlowRow( 87 | modifier = Modifier.padding(top = 10.dp) 88 | ) { 89 | for (i in 0..7) { 90 | LabelTextButton( 91 | text = "android", 92 | modifier = Modifier.padding(start = 5.dp, bottom = 5.dp), 93 | isLoading = true 94 | ) 95 | } 96 | } 97 | Spacer(modifier = Modifier.height(10.dp)) 98 | } else { 99 | if (!wrapper.articles.isNullOrEmpty()) { 100 | FlowRow( 101 | modifier = Modifier.padding(top = 10.dp) 102 | ) { 103 | for (item in wrapper.articles!!) { 104 | LabelTextButton( 105 | text = item.title ?: "android", 106 | modifier = Modifier.padding(start = 5.dp, bottom = 5.dp), 107 | onClick = { 108 | val webData = WebData(item.title, item.link!!) 109 | onSelected(webData) 110 | } 111 | ) 112 | } 113 | } 114 | Spacer(modifier = Modifier.height(10.dp)) 115 | } 116 | } 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/category/navigation/NaviViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.category.navigation 2 | 3 | import com.mm.hamcompose.data.bean.NaviWrapper 4 | import com.mm.hamcompose.data.http.HttpResult 5 | import com.mm.hamcompose.repository.HttpRepository 6 | import com.mm.hamcompose.ui.page.base.BaseViewModel 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import kotlinx.coroutines.flow.collectLatest 9 | import javax.inject.Inject 10 | 11 | @HiltViewModel 12 | class NaviViewModel @Inject constructor( 13 | private val httpRepo: HttpRepository 14 | ): BaseViewModel() { 15 | 16 | override fun loadContent() { 17 | async { 18 | httpRepo.getNavigationList().collectLatest { response -> 19 | when (response) { 20 | is HttpResult.Success -> { 21 | list.value = response.result 22 | } 23 | is HttpResult.Error -> { 24 | 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | override fun start() { 32 | initThat { loadContent() } 33 | } 34 | 35 | 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/category/pubaccount/author/PublicAccountAuthorViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.category.pubaccount.author 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.paging.cachedIn 7 | import com.blankj.utilcode.util.LogUtils 8 | import com.mm.hamcompose.data.bean.Article 9 | import com.mm.hamcompose.data.db.history.HistoryDatabase 10 | import com.mm.hamcompose.repository.HttpRepository 11 | import com.mm.hamcompose.repository.PagingArticle 12 | import com.mm.hamcompose.ui.page.base.BaseCollectViewModel 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class PublicAccountAuthorViewModel @Inject constructor( 18 | private var repo: HttpRepository, 19 | private val db: HistoryDatabase, 20 | ): BaseCollectViewModel
(repo) { 21 | 22 | /** 23 | * 某个技术公众号的列表 24 | */ 25 | var publicData = MutableLiveData(null) 26 | var isRefreshing = mutableStateOf(false) 27 | private var authorId = mutableStateOf(-1) 28 | 29 | override fun start() { 30 | initThat { initPublicArticles() } 31 | } 32 | 33 | fun setPublicId(id: Int) { 34 | authorId.value = id 35 | } 36 | 37 | fun clearCache() { 38 | isRefreshing.value = true 39 | publicData.value = null 40 | } 41 | 42 | fun initPublicArticles() { 43 | if (publicData.value==null) { 44 | publicData.value = getPublicArticles() 45 | isRefreshing.value = publicData.value==null 46 | } 47 | } 48 | 49 | private fun getPublicArticles() = repo.getPublicArticles(authorId.value).cachedIn(viewModelScope) 50 | 51 | override fun onCleared() { 52 | LogUtils.e("ViewModel执行onCleared()") 53 | super.onCleared() 54 | } 55 | 56 | fun saveDataToHistory(article: Article) { 57 | cacheHistory(db, article) 58 | } 59 | 60 | 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/category/pubaccount/category/PublicAccountViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.category.pubaccount.category 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import com.mm.hamcompose.data.bean.ParentBean 5 | import com.mm.hamcompose.data.http.HttpResult 6 | import com.mm.hamcompose.repository.HttpRepository 7 | import com.mm.hamcompose.ui.page.base.BaseViewModel 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.collectLatest 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class PublicAccountViewModel @Inject constructor( 14 | private var httpRepo: HttpRepository 15 | ): BaseViewModel() { 16 | 17 | //公众号ID 18 | private var publicId = mutableStateOf(-1) 19 | 20 | override fun start() { 21 | initThat { loadContent() } 22 | } 23 | 24 | override fun loadContent() { 25 | async { 26 | httpRepo.getPublicInformation().collectLatest { response -> 27 | when (response) { 28 | is HttpResult.Success -> { 29 | list.value = response.result 30 | } 31 | is HttpResult.Error -> { 32 | 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/category/pubaccount/search/PublicAccountSearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.category.pubaccount.search 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.paging.cachedIn 7 | import com.mm.hamcompose.data.bean.Article 8 | import com.mm.hamcompose.data.db.history.HistoryDatabase 9 | import com.mm.hamcompose.repository.HttpRepository 10 | import com.mm.hamcompose.repository.PagingArticle 11 | import com.mm.hamcompose.ui.page.base.BaseCollectViewModel 12 | import dagger.hilt.android.lifecycle.HiltViewModel 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class PublicAccountSearchViewModel @Inject constructor( 17 | private val repo: HttpRepository, 18 | private val db: HistoryDatabase, 19 | ): BaseCollectViewModel
(repo) { 20 | 21 | /** 22 | * 在某个公众号下,搜索关键字 23 | */ 24 | //var searchText = MutableLiveData("") 25 | var isRefreshing = mutableStateOf(false) 26 | var searchContent = mutableStateOf("") 27 | val searchResult = MutableLiveData(null) 28 | private var publicId = mutableStateOf(-1) 29 | 30 | 31 | override fun start() { 32 | 33 | } 34 | 35 | fun setPublicId(id: Int) { 36 | this.publicId.value = id 37 | } 38 | 39 | private fun searchArticleWithKey(key: String) = 40 | repo.searchArticleWithKey(publicId.value, key).cachedIn(viewModelScope) 41 | 42 | fun refreshSearch(key: String) { 43 | resetListIndex() 44 | search(key) 45 | } 46 | 47 | fun search(key: String) { 48 | searchResult.value = null 49 | searchResult.value = searchArticleWithKey(key) 50 | isRefreshing.value = searchResult.value==null 51 | } 52 | 53 | fun saveDataToHistory(article: Article) { 54 | cacheHistory(db, article) 55 | } 56 | 57 | 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/category/share/ShareArticlePage.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.category.share 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.material.ScaffoldState 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.ExperimentalComposeUiApi 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.unit.dp 14 | import androidx.hilt.navigation.compose.hiltViewModel 15 | import androidx.navigation.NavHostController 16 | import com.mm.hamcompose.R 17 | import com.mm.hamcompose.ui.route.RouteUtils.back 18 | import com.mm.hamcompose.ui.widget.* 19 | 20 | @OptIn(ExperimentalComposeUiApi::class) 21 | @Composable 22 | fun ShareArticlePage( 23 | navCtrl: NavHostController, 24 | scaffoldState: ScaffoldState, 25 | viewModel: ShareArticleViewModel = hiltViewModel() 26 | ) { 27 | 28 | val title by remember { viewModel.title } 29 | val shareUser by remember { viewModel.shareUser } 30 | val linkUrl by remember { viewModel.linkUrl } 31 | var errorMsg by remember { viewModel.errorMessage } 32 | var snackLabel by remember { mutableStateOf(SNACK_WARN) } 33 | val keyboardController = LocalSoftwareKeyboardController.current 34 | 35 | 36 | if (errorMsg.isNotEmpty()) { 37 | popupSnackBar( 38 | scope = rememberCoroutineScope(), 39 | scaffoldState = scaffoldState, 40 | label = if (errorMsg == "分享成功") SNACK_SUCCESS else snackLabel, 41 | message = errorMsg 42 | ) 43 | errorMsg = "" 44 | } 45 | 46 | Column { 47 | HamToolBar( 48 | title = "分享文章", 49 | onBack = { navCtrl.back() }, 50 | rightText = "保存", 51 | onRightClick = { 52 | keyboardController?.hide() 53 | if (title.isNullOrEmpty()) { 54 | snackLabel = SNACK_WARN 55 | errorMsg = "标题不能为空" 56 | return@HamToolBar 57 | } 58 | if (linkUrl.isNullOrEmpty()) { 59 | snackLabel = SNACK_WARN 60 | errorMsg = "链接不能为空" 61 | return@HamToolBar 62 | } 63 | viewModel.addShareArticle() 64 | } 65 | ) 66 | 67 | LazyColumn( 68 | modifier = Modifier.weight(1f) 69 | ) { 70 | item { 71 | LabelEditView( 72 | modifier = Modifier.padding(horizontal = 10.dp, vertical = 20.dp), 73 | text = title ?: "", 74 | labelText = stringResource(id = R.string.title), 75 | hintText = "请输入标题(限100字以内)", 76 | onValueChanged = { 77 | viewModel.title.value = it 78 | }, 79 | onDeleteClick = { viewModel.title.value = "" }, 80 | ) 81 | } 82 | item { 83 | LabelEditView( 84 | modifier = Modifier.padding(horizontal = 10.dp, vertical = 20.dp), 85 | text = shareUser ?: "", 86 | labelText = stringResource(id = R.string.author), 87 | hintText = "默认使用昵称,没有昵称则使用用户名", 88 | onValueChanged = { 89 | viewModel.shareUser.value = it 90 | }, 91 | onDeleteClick = { viewModel.shareUser.value = "" }, 92 | ) 93 | } 94 | item { 95 | LabelEditView( 96 | modifier = Modifier.padding(horizontal = 10.dp, vertical = 20.dp), 97 | text = linkUrl ?: "", 98 | labelText = stringResource(id = R.string.link), 99 | hintText = stringResource(id = R.string.hint_text_website_url), 100 | onValueChanged = { 101 | viewModel.linkUrl.value = it 102 | }, 103 | onDeleteClick = { viewModel.linkUrl.value = "" }, 104 | ) 105 | } 106 | } 107 | } 108 | 109 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/category/share/ShareArticleViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.category.share 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import com.mm.hamcompose.data.bean.Article 5 | import com.mm.hamcompose.data.db.user.UserInfoDatabase 6 | import com.mm.hamcompose.data.http.HttpResult 7 | import com.mm.hamcompose.repository.HttpRepository 8 | import com.mm.hamcompose.ui.page.base.BaseViewModel 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.flow.collectLatest 11 | import javax.inject.Inject 12 | 13 | @HiltViewModel 14 | class ShareArticleViewModel @Inject constructor( 15 | private val repo: HttpRepository, 16 | private val db: UserInfoDatabase 17 | ): BaseViewModel
() { 18 | 19 | var title = mutableStateOf(null) 20 | var shareUser = mutableStateOf(null) 21 | var linkUrl = mutableStateOf(null) 22 | var errorMessage = mutableStateOf("") 23 | 24 | override fun start() { 25 | 26 | } 27 | 28 | fun addShareArticle() { 29 | async { 30 | if (shareUser.value.isNullOrEmpty()) { 31 | val users = db.userInfoDao().queryUserInfo() 32 | if (!users.isNullOrEmpty()) { 33 | with(users[0]!!) { 34 | shareUser.value = if (nickname.isEmpty()) username else nickname 35 | } 36 | } 37 | } 38 | repo.addMyShareArticle(title.value!!, linkUrl.value!!, shareUser.value!!) 39 | .collectLatest { response -> 40 | when(response) { 41 | is HttpResult.Success -> { } 42 | is HttpResult.Error -> { 43 | val isShare = response.exception.message == "the result of remote's request is null" 44 | if (isShare) { 45 | errorMessage.value = "分享成功" 46 | } 47 | } 48 | } 49 | } 50 | 51 | } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/category/structure/list/StructureListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.category.structure.list 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.paging.cachedIn 7 | import com.mm.hamcompose.data.bean.Article 8 | import com.mm.hamcompose.data.db.history.HistoryDatabase 9 | import com.mm.hamcompose.repository.HttpRepository 10 | import com.mm.hamcompose.repository.PagingArticle 11 | import com.mm.hamcompose.ui.page.base.BaseCollectViewModel 12 | import dagger.hilt.android.lifecycle.HiltViewModel 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class StructureListViewModel @Inject constructor( 17 | private val repo: HttpRepository, 18 | private val db: HistoryDatabase, 19 | ): BaseCollectViewModel
(repo) { 20 | 21 | private var cid = -1 22 | //某个文章的列表 23 | var articles = MutableLiveData(null) 24 | var isRefreshing = mutableStateOf(true) 25 | var authorName = mutableStateOf("") 26 | 27 | fun setId(id: Int) { 28 | cid = id 29 | } 30 | 31 | override fun start() { 32 | initThat { initArticles() } 33 | } 34 | 35 | fun refresh(author: String) { 36 | resetListIndex() 37 | isRefreshing.value = true 38 | articles.value = null 39 | if (author.isEmpty()) { 40 | initArticles() 41 | } else { 42 | searchByAuthor(author) 43 | } 44 | } 45 | 46 | private fun initArticles() { 47 | if (articles.value==null) { 48 | articles.value = getStructureArticles() 49 | isRefreshing.value = articles.value==null 50 | } 51 | } 52 | 53 | private fun getStructureArticles() = repo.getStructureArticles(cid).cachedIn(viewModelScope) 54 | 55 | fun searchByAuthor(author: String) { 56 | isRefreshing.value = true 57 | articles.value = null 58 | articles.value = repo.getStructureArticles(author).cachedIn(viewModelScope) 59 | isRefreshing.value = articles.value==null 60 | } 61 | 62 | 63 | fun saveDataToHistory(article: Article) { 64 | cacheHistory(db, article) 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/category/structure/tree/StructureTreePage.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.category.structure.tree 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.lazy.itemsIndexed 8 | import androidx.compose.foundation.lazy.rememberLazyListState 9 | import androidx.compose.material.Divider 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.text.font.FontWeight 16 | import androidx.compose.ui.unit.dp 17 | import androidx.hilt.navigation.compose.hiltViewModel 18 | import androidx.navigation.NavHostController 19 | import com.google.accompanist.flowlayout.FlowRow 20 | import com.mm.hamcompose.data.bean.ParentBean 21 | import com.mm.hamcompose.theme.HamTheme 22 | import com.mm.hamcompose.ui.route.RouteName 23 | import com.mm.hamcompose.ui.route.RouteUtils 24 | import com.mm.hamcompose.ui.widget.LabelTextButton 25 | import com.mm.hamcompose.ui.widget.ListTitle 26 | 27 | @OptIn(ExperimentalFoundationApi::class) 28 | @Composable 29 | fun StructurePage( 30 | navCtrl: NavHostController, 31 | viewModel: StructureViewModel = hiltViewModel() 32 | ) { 33 | 34 | viewModel.start() 35 | val systemData by remember { viewModel.list } 36 | val isLoading by remember { viewModel.loading } 37 | val currentPosition by remember { viewModel.currentListIndex } 38 | val listState = rememberLazyListState(currentPosition) 39 | 40 | LazyColumn( 41 | modifier = Modifier 42 | .fillMaxWidth() 43 | .fillMaxHeight() 44 | .background(HamTheme.colors.background), 45 | state = listState, 46 | contentPadding = PaddingValues(vertical = 10.dp) 47 | ) { 48 | 49 | if (isLoading) { 50 | items(5) { 51 | StructureItem(ParentBean(null), isLoading = true) 52 | } 53 | } else { 54 | systemData.forEachIndexed { position, chapter1 -> 55 | stickyHeader { ListTitle(title = chapter1.name ?: "标题") } 56 | item { 57 | StructureItem(chapter1, onSelect = { parent-> 58 | viewModel.savePosition(listState.firstVisibleItemIndex) 59 | RouteUtils.navTo(navCtrl, RouteName.STRUCTURE_LIST, parent) 60 | }) 61 | if (position <= systemData.size - 1) { 62 | Divider(startIndent = 10.dp, color = HamTheme.colors.divider, thickness = 0.8f.dp) 63 | } 64 | Spacer(modifier = Modifier.height(10.dp)) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | @Composable 72 | fun StructureItem( 73 | bean: ParentBean, 74 | isLoading: Boolean = false, 75 | onSelect: (parent: ParentBean) -> Unit = {}, 76 | ) { 77 | Column( 78 | modifier = Modifier.fillMaxWidth().padding(top = 10.dp) 79 | ) { 80 | if (isLoading) { 81 | ListTitle(title = "我都标题", isLoading = true) 82 | FlowRow( 83 | modifier = Modifier.padding(horizontal = 10.dp) 84 | ) { 85 | for (i in 0..7) { 86 | LabelTextButton( 87 | text = "android", 88 | modifier = Modifier.padding(start = 5.dp, bottom = 5.dp), 89 | isLoading = true 90 | ) 91 | } 92 | } 93 | Spacer(modifier = Modifier.height(10.dp)) 94 | } else { 95 | if (!bean.children.isNullOrEmpty()) { 96 | FlowRow( 97 | modifier = Modifier.padding(horizontal = 10.dp) 98 | ) { 99 | for (item in bean.children!!) { 100 | LabelTextButton( 101 | text = item.name ?: "android", 102 | modifier = Modifier.padding(start = 5.dp, bottom = 5.dp), 103 | onClick = { 104 | onSelect(item) 105 | } 106 | ) 107 | } 108 | } 109 | Spacer(modifier = Modifier.height(10.dp)) 110 | } 111 | } 112 | 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/category/structure/tree/StructureTreeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.category.structure.tree 2 | 3 | import com.mm.hamcompose.data.bean.ParentBean 4 | import com.mm.hamcompose.data.http.HttpResult 5 | import com.mm.hamcompose.repository.HttpRepository 6 | import com.mm.hamcompose.ui.page.base.BaseViewModel 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import kotlinx.coroutines.flow.collectLatest 9 | import javax.inject.Inject 10 | 11 | private const val TAG = "StructureViewModel ==> " 12 | 13 | @HiltViewModel 14 | class StructureViewModel @Inject constructor( 15 | private var repo: HttpRepository, 16 | ): BaseViewModel() { 17 | 18 | override fun loadContent() { 19 | startLoading() 20 | async { 21 | repo.getStructureList().collectLatest { response -> 22 | when (response) { 23 | is HttpResult.Success -> { 24 | list.value = response.result 25 | } 26 | is HttpResult.Error -> { 27 | println(TAG + response.exception.message) 28 | } 29 | } 30 | stopLoading() 31 | } 32 | } 33 | } 34 | 35 | override fun start() { 36 | initThat { loadContent() } 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/collection/CollectionViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.collection 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.viewModelScope 7 | import androidx.paging.cachedIn 8 | import com.mm.hamcompose.data.bean.ParentBean 9 | import com.mm.hamcompose.data.bean.TabTitle 10 | import com.mm.hamcompose.data.db.user.UserInfoDatabase 11 | import com.mm.hamcompose.data.http.HttpResult 12 | import com.mm.hamcompose.repository.HttpRepository 13 | import com.mm.hamcompose.repository.PagingCollect 14 | import com.mm.hamcompose.ui.page.base.BaseViewModel 15 | import dagger.hilt.android.lifecycle.HiltViewModel 16 | import kotlinx.coroutines.Dispatchers 17 | import kotlinx.coroutines.flow.* 18 | import javax.inject.Inject 19 | 20 | @HiltViewModel 21 | class CollectionViewModel @Inject constructor( 22 | private val repo: HttpRepository, 23 | private val db: UserInfoDatabase 24 | ) : BaseViewModel() { 25 | 26 | val titles = mutableStateOf( 27 | mutableListOf( 28 | TabTitle(301, "文章列表"), 29 | TabTitle(302, "我的网址"), 30 | ) 31 | ) 32 | 33 | var collectArticles = MutableLiveData(null) 34 | var webUrlList = mutableStateOf?>(null) 35 | var isRefreshing = mutableStateOf(false) 36 | var isLogin = mutableStateOf(false) 37 | 38 | override fun start() { 39 | checkLoginState() 40 | } 41 | 42 | private fun checkLoginState() { 43 | async { 44 | flow { emit(db.userInfoDao().queryUserInfo()) } 45 | .flowOn(Dispatchers.IO) 46 | .collectLatest { users -> 47 | isLogin.value = users.isNotEmpty() 48 | if (isLogin.value && isNotInit()) { 49 | initData() 50 | } 51 | } 52 | } 53 | } 54 | 55 | private fun isNotInit() = collectArticles.value == null && webUrlList.value == null 56 | 57 | private fun initData() { 58 | getCollectUrlList() 59 | getArticles() 60 | } 61 | 62 | fun refresh() { 63 | isRefreshing.value = true 64 | webUrlList.value = null 65 | collectArticles.value = null 66 | checkLoginState() 67 | } 68 | 69 | private fun getArticles() { 70 | collectArticles.value = collectList() 71 | isRefreshing.value = collectArticles.value == null 72 | } 73 | 74 | private fun getCollectUrlList() { 75 | async { 76 | repo.getCollectUrls().collectLatest { response -> 77 | when (response) { 78 | is HttpResult.Success -> { 79 | webUrlList.value = response.result 80 | } 81 | is HttpResult.Error -> { 82 | 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | private fun collectList() = repo.getCollectionList().cachedIn(viewModelScope) 90 | 91 | fun uncollectArticle(id: Int, originId: Int) { 92 | async { 93 | repo.uncollectArticleById(id, originId).collectLatest { response -> 94 | when (response) { 95 | is HttpResult.Success -> { 96 | } 97 | is HttpResult.Error -> { 98 | //收藏接口,不走success判断分支 99 | val deleted = response.exception.message == "the result of remote's request is null" 100 | if (deleted) { 101 | println("取消收藏(id=$id)") 102 | getArticles() 103 | message.value = "已取消收藏" 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | @SuppressLint("NewApi") 112 | fun deleteWebsite(id: Int) { 113 | async { 114 | repo.deleteWebsite(id).collectLatest { response -> 115 | when (response) { 116 | is HttpResult.Success -> { 117 | } 118 | is HttpResult.Error -> { 119 | if (response.exception.message == "the result of remote's request is null") { 120 | webUrlList.value?.remove(webUrlList.value?.find { it.id == id }) 121 | message.value = "删除成功" 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | override fun onCleared() { 130 | super.onCleared() 131 | println("CollectionViewModel ==> onClear") 132 | } 133 | 134 | 135 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/collection/edit/WebSiteEditViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.collection.edit 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import com.mm.hamcompose.data.bean.TabTitle 5 | import com.mm.hamcompose.data.http.HttpResult 6 | import com.mm.hamcompose.repository.HttpRepository 7 | import com.mm.hamcompose.ui.page.base.BaseViewModel 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.collectLatest 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class WebSiteEditViewModel @Inject constructor( 14 | private val repo: HttpRepository 15 | ): BaseViewModel() { 16 | 17 | var webSiteTitle = mutableStateOf(null) 18 | var linkUrl = mutableStateOf(null) 19 | var author = mutableStateOf(null) 20 | var isSaved = mutableStateOf(false) 21 | var errorMessage = mutableStateOf("") 22 | val titles = mutableStateOf(mutableListOf( 23 | TabTitle(601, "网站"), 24 | TabTitle(602, "文章"), 25 | )) 26 | 27 | override fun start() { 28 | 29 | } 30 | 31 | /** 32 | * type: 0 = 添加新网站, 1 = 添加新文章 , -1 = 编辑网站 33 | * id: 仅编辑网站时候用到此参数 34 | */ 35 | fun saveNewCollect(type: Int, id: Int = 0) { 36 | async { 37 | when (type) { 38 | 0 -> { 39 | repo.addNewWebsiteCollect(webSiteTitle.value!!, linkUrl.value!!) 40 | .collectLatest { response -> 41 | when (response) { 42 | is HttpResult.Success -> { 43 | isSaved.value = true 44 | } 45 | is HttpResult.Error -> { 46 | println(response.exception.message) 47 | errorMessage.value = response.exception.message ?: "请求异常" 48 | } 49 | } 50 | } 51 | } 52 | 1 -> { 53 | repo.addNewArticleCollect(webSiteTitle.value!!, linkUrl.value!!, author.value!!) 54 | .collectLatest { response -> 55 | when (response) { 56 | is HttpResult.Success -> { 57 | isSaved.value = true 58 | } 59 | is HttpResult.Error -> { 60 | println(response.exception.message) 61 | errorMessage.value = response.exception.message ?: "请求异常" 62 | } 63 | } 64 | } 65 | } 66 | else -> { 67 | repo.editCollectWebsite(id, webSiteTitle.value!!, linkUrl.value!!) 68 | .collectLatest { response -> 69 | when (response) { 70 | is HttpResult.Success -> { 71 | isSaved.value = true 72 | } 73 | is HttpResult.Error -> { 74 | println(response.exception.message) 75 | errorMessage.value = response.exception.message ?: "请求异常" 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | private fun setResponseState(errorInfo: String?) { 85 | isSaved.value = errorInfo == "the result of remote's request is null" 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.home 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import com.mm.hamcompose.R 5 | import com.mm.hamcompose.data.bean.HomeThemeBean 6 | import com.mm.hamcompose.data.bean.MenuTitle 7 | import com.mm.hamcompose.data.bean.TabTitle 8 | import com.mm.hamcompose.theme.HamTheme 9 | import com.mm.hamcompose.ui.page.base.BaseViewModel 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import javax.inject.Inject 12 | 13 | @HiltViewModel 14 | class HomeViewModel @Inject constructor() : BaseViewModel() { 15 | 16 | var theme = mutableStateOf(HamTheme.Theme.Light) 17 | var menuItems = mutableListOf( 18 | MenuTitle("主页", null), 19 | MenuTitle("福利", R.drawable.ic_menu_welfare), 20 | MenuTitle("收藏", R.drawable.ic_star), 21 | MenuTitle("设置", R.drawable.ic_menu_settings), 22 | ) 23 | 24 | var isShowSearchBar = mutableStateOf(true) 25 | val titles = mutableStateOf( 26 | mutableListOf( 27 | TabTitle(101, "推荐"), 28 | TabTitle(102, "广场"), 29 | TabTitle(103, "项目"), 30 | TabTitle(104, "问答") 31 | ) 32 | ) 33 | 34 | fun setCachePosition(tabIndex: Int, newPosition: Int) { 35 | titles.value[tabIndex].cachePosition = newPosition 36 | //val oldPosition = titles.value[tabIndex].cachePosition 37 | //isShowSearchBar.value = newPosition <= 1 38 | //LogUtils.w("newPosition = $newPosition isShowSearch = ${isShowSearchBar.value}") 39 | } 40 | 41 | override fun start() { 42 | 43 | } 44 | 45 | override fun onCleared() { 46 | super.onCleared() 47 | println("HomeViewModel ==> onClear") 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/home/index/IndexViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.home.index 2 | 3 | import androidx.compose.runtime.mutableStateListOf 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.viewModelScope 7 | import androidx.paging.cachedIn 8 | import com.blankj.utilcode.util.LogUtils 9 | import com.mm.hamcompose.data.bean.Article 10 | import com.mm.hamcompose.data.bean.BannerBean 11 | import com.mm.hamcompose.data.db.history.HistoryDatabase 12 | import com.mm.hamcompose.data.http.HttpResult 13 | import com.mm.hamcompose.repository.HttpRepository 14 | import com.mm.hamcompose.repository.PagingArticle 15 | import com.mm.hamcompose.ui.page.base.BaseCollectViewModel 16 | import com.mm.hamcompose.ui.widget.BannerData 17 | import dagger.hilt.android.lifecycle.HiltViewModel 18 | import kotlinx.coroutines.flow.collectLatest 19 | import javax.inject.Inject 20 | 21 | 22 | @HiltViewModel 23 | class IndexViewModel @Inject constructor( 24 | private var repo: HttpRepository, 25 | private val historyDb: HistoryDatabase 26 | ) : BaseCollectViewModel(repo) { 27 | 28 | var pagingData = MutableLiveData(null) 29 | val imageList = mutableStateOf(mutableListOf()) 30 | var isRefreshing = mutableStateOf(false) 31 | var topArticles = mutableStateListOf
() 32 | 33 | //列表:使用paging3分页加载框架 34 | private fun homeData() = repo.getIndexData().cachedIn(viewModelScope) 35 | 36 | private fun loadBanners() { 37 | async { 38 | repo.getBanners().collectLatest { response -> 39 | when (response) { 40 | is HttpResult.Success -> { 41 | imageList.value = response.result.map { 42 | BannerData( 43 | imageUrl = it.imagePath ?: "", 44 | linkUrl = it.url ?: "", 45 | title = it.title ?: "" 46 | ) 47 | } as MutableList 48 | } 49 | is HttpResult.Error -> { 50 | imageList.value.clear() 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | private fun loadTopArticles() { 58 | async { 59 | repo.getTopArticles().collectLatest { response -> 60 | when (response) { 61 | is HttpResult.Success -> { 62 | topArticles.clear() 63 | topArticles.addAll(response.result) 64 | } 65 | is HttpResult.Error -> { 66 | topArticles.clear() 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | private fun refreshBanner() { 74 | imageList.value.clear() 75 | loadBanners() 76 | } 77 | 78 | private fun refreshHots() { 79 | topArticles.clear() 80 | loadTopArticles() 81 | } 82 | 83 | private fun getHomesList() { 84 | pagingData.value = null 85 | pagingData.value = homeData() 86 | isRefreshing.value = pagingData.value == null 87 | } 88 | 89 | fun refresh() { 90 | resetListIndex() 91 | isRefreshing.value = true 92 | refreshBanner() 93 | refreshHots() 94 | getHomesList() 95 | } 96 | 97 | override fun start() { 98 | initThat { 99 | loadBanners() 100 | loadTopArticles() 101 | getHomesList() 102 | } 103 | } 104 | 105 | override fun onCleared() { 106 | LogUtils.e("IndexViewModel ===> ViewModel执行onCleared()") 107 | super.onCleared() 108 | } 109 | 110 | fun saveDataToHistory(article: Article) { 111 | cacheHistory(historyDb, article) 112 | } 113 | 114 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/home/project/ProjectViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.home.project 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.paging.cachedIn 7 | import com.mm.hamcompose.data.bean.ParentBean 8 | import com.mm.hamcompose.data.http.HttpResult 9 | import com.mm.hamcompose.repository.HttpRepository 10 | import com.mm.hamcompose.repository.PagingArticle 11 | import com.mm.hamcompose.ui.page.base.BaseCollectViewModel 12 | import dagger.hilt.android.lifecycle.HiltViewModel 13 | import kotlinx.coroutines.flow.catch 14 | import kotlinx.coroutines.flow.collectLatest 15 | import kotlinx.coroutines.flow.onCompletion 16 | import javax.inject.Inject 17 | 18 | @HiltViewModel 19 | class ProjectViewModel @Inject constructor( 20 | private var httpRepo: HttpRepository 21 | ): BaseCollectViewModel(httpRepo) { 22 | 23 | var tabIndex = mutableStateOf(-1) 24 | var isRefreshing = mutableStateOf(true) 25 | var pagingData = MutableLiveData(null) 26 | var projectId = mutableStateOf(-1) 27 | var currentRowIndex = mutableStateOf(0) 28 | 29 | override fun start() { 30 | initThat { 31 | loadCategory() 32 | loadContent() 33 | } 34 | 35 | } 36 | 37 | fun setupProjectId(id: Int) { 38 | this.projectId.value = id 39 | } 40 | 41 | /** 42 | * 触发刷新机制 43 | */ 44 | fun triggerRefresh() { 45 | isRefreshing.value = true 46 | } 47 | 48 | fun setTabIndex(index: Int) { 49 | tabIndex.value = index 50 | } 51 | 52 | private fun loadCategory() { 53 | async { 54 | httpRepo.getProjectCategory() 55 | .collectLatest { response -> 56 | when (response) { 57 | is HttpResult.Success -> { 58 | list.value = response.result 59 | tabIndex.value = 0 60 | } 61 | is HttpResult.Error -> { 62 | println(response.exception.message) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | private fun getProjects() = httpRepo.getProjects(projectId.value).cachedIn(viewModelScope) 70 | 71 | fun refresh() { 72 | pagingData.value = null 73 | loadContent() 74 | } 75 | 76 | override fun loadContent() { 77 | pagingData.value = getProjects() 78 | isRefreshing.value = pagingData.value==null 79 | } 80 | 81 | fun saveRowPosition(position: Int) { 82 | currentRowIndex.value = position 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/home/search/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.home.search 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.paging.cachedIn 7 | import com.blankj.utilcode.util.LogUtils 8 | import com.mm.hamcompose.data.bean.Article 9 | import com.mm.hamcompose.data.bean.Hotkey 10 | import com.mm.hamcompose.data.db.history.HistoryDatabase 11 | import com.mm.hamcompose.data.db.hotkey.HotkeyDatabase 12 | import com.mm.hamcompose.data.http.HttpResult 13 | import com.mm.hamcompose.repository.HttpRepository 14 | import com.mm.hamcompose.repository.PagingArticle 15 | import com.mm.hamcompose.ui.page.base.BaseCollectViewModel 16 | import dagger.hilt.android.lifecycle.HiltViewModel 17 | import kotlinx.coroutines.Dispatchers 18 | import kotlinx.coroutines.flow.collectLatest 19 | import kotlinx.coroutines.withContext 20 | import javax.inject.Inject 21 | 22 | @HiltViewModel 23 | class SearchViewModel @Inject constructor( 24 | private val repo: HttpRepository, 25 | private val hotkeyDatabase: HotkeyDatabase, 26 | private val historyDatabase: HistoryDatabase, 27 | ): BaseCollectViewModel
(repo) { 28 | 29 | //搜索列表 30 | val searches = MutableLiveData(null) 31 | //搜索词的历史记录 32 | val history = mutableStateOf(mutableListOf()) 33 | //搜索的热词 34 | val hotkeys = mutableStateOf(mutableListOf()) 35 | val searchContent = mutableStateOf("") 36 | 37 | override fun start() { 38 | initThat { 39 | getHotkey() 40 | getHistory() 41 | } 42 | } 43 | 44 | fun search(key: String) { 45 | searches.value = repo.queryArticle(key).cachedIn(viewModelScope) 46 | } 47 | 48 | //插入数据 49 | fun insertKey(key: String) { 50 | if (hasTheSame(key)) 51 | return 52 | async { 53 | val bean = Hotkey(link = "", name = key, order = 1, visible = 0) 54 | hotkeyDatabase.hotkeyDao().insertHotkeys(bean) 55 | update() 56 | } 57 | } 58 | 59 | //判断是否已经存在搜索词 60 | private fun hasTheSame(key: String): Boolean { 61 | val same = history.value.indexOfFirst { it.name.equals(key, ignoreCase = true) } != -1 62 | LogUtils.e("是否相同搜索词 = $same") 63 | return same 64 | } 65 | 66 | //删除单条数据 67 | fun deleteKey(key: Hotkey) { 68 | async { 69 | withContext(Dispatchers.IO) { 70 | hotkeyDatabase.hotkeyDao().deleteKeys(key) 71 | } 72 | update() 73 | } 74 | } 75 | 76 | //删除所有数据 77 | fun deleteAll() { 78 | async { 79 | hotkeyDatabase.hotkeyDao().deleteAll() 80 | update() 81 | } 82 | } 83 | 84 | private fun update() = getHistory() 85 | 86 | //查询所有数据 87 | fun getHistory() { 88 | async { 89 | val result = hotkeyDatabase.hotkeyDao().loadAllKeys() 90 | withContext(Dispatchers.Main) { 91 | history.value = result.toMutableList() 92 | } 93 | } 94 | } 95 | 96 | fun getHotkey() { 97 | async { 98 | repo.getHotkeys().collectLatest { response -> 99 | when (response) { 100 | is HttpResult.Success -> { 101 | hotkeys.value = response.result 102 | } 103 | is HttpResult.Error -> { 104 | 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | fun saveDataToHistory(article: Article) { 112 | cacheHistory(historyDatabase, article) 113 | } 114 | 115 | 116 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/home/square/SquarePage.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.home.square 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.foundation.lazy.rememberLazyListState 7 | import androidx.compose.material.ScaffoldState 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.rememberCoroutineScope 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import androidx.hilt.navigation.compose.hiltViewModel 15 | import androidx.navigation.NavHostController 16 | import androidx.paging.compose.collectAsLazyPagingItems 17 | import androidx.paging.compose.itemsIndexed 18 | import com.google.accompanist.swiperefresh.SwipeRefresh 19 | import com.google.accompanist.swiperefresh.rememberSwipeRefreshState 20 | import com.mm.hamcompose.data.bean.Article 21 | import com.mm.hamcompose.ui.route.RouteName 22 | import com.mm.hamcompose.ui.route.RouteUtils 23 | import com.mm.hamcompose.ui.widget.EmptyView 24 | import com.mm.hamcompose.ui.widget.MultiStateItemView 25 | import com.mm.hamcompose.ui.widget.SNACK_INFO 26 | import com.mm.hamcompose.ui.widget.popupSnackBar 27 | 28 | @Composable 29 | fun SquarePage( 30 | navCtrl: NavHostController, 31 | scaffoldState: ScaffoldState, 32 | viewModel: SquareViewModel = hiltViewModel(), 33 | onScrollChangeListener: (position: Int) -> Unit, 34 | ) { 35 | 36 | viewModel.start() 37 | val squareData = viewModel.pagingData.value?.collectAsLazyPagingItems() 38 | val isLoaded = squareData?.loadState?.prepend?.endOfPaginationReached ?: false 39 | val refreshing: Boolean by remember { viewModel.isRefreshing } 40 | val swipeRefreshState = rememberSwipeRefreshState(refreshing) 41 | val currentPosition by remember { viewModel.currentListIndex } 42 | val message by remember { viewModel.message } 43 | val listState = rememberLazyListState(currentPosition) 44 | val coroutineScope = rememberCoroutineScope() 45 | 46 | if (message.isNotEmpty()) { 47 | popupSnackBar(coroutineScope, scaffoldState, SNACK_INFO, message) 48 | viewModel.message.value = "" 49 | } 50 | 51 | 52 | SwipeRefresh( 53 | state = swipeRefreshState, 54 | onRefresh = { viewModel.refresh() } 55 | ) { 56 | 57 | LazyColumn( 58 | modifier = Modifier.fillMaxSize(), 59 | state = listState, 60 | contentPadding = PaddingValues(top = 10.dp) 61 | ) { 62 | 63 | if (isLoaded) { 64 | if (squareData!!.itemCount > 0) { 65 | itemsIndexed(squareData) { index, item -> 66 | MultiStateItemView( 67 | data = item!!, 68 | onSelected = { 69 | viewModel.saveDataToHistory(item) 70 | viewModel.savePosition(listState.firstVisibleItemIndex) 71 | RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, it) 72 | }, 73 | onCollectClick = { 74 | if (item.collect) { 75 | viewModel.uncollectArticleById(it) 76 | squareData.peek(index)?.collect = false 77 | } else { 78 | viewModel.collectArticleById(it) 79 | squareData.peek(index)?.collect = true 80 | } 81 | 82 | }, 83 | onUserClick = { userId -> 84 | RouteUtils.navTo(navCtrl, RouteName.SHARER, userId) 85 | }) 86 | } 87 | onScrollChangeListener(listState.firstVisibleItemIndex) 88 | } else { 89 | item { EmptyView() } 90 | } 91 | } else { 92 | items(5) { 93 | MultiStateItemView( 94 | data = Article(), 95 | isLoading = true 96 | ) 97 | } 98 | } 99 | 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/home/square/SquareViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.home.square 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.paging.cachedIn 7 | import com.blankj.utilcode.util.LogUtils 8 | import com.mm.hamcompose.data.bean.Article 9 | import com.mm.hamcompose.data.db.history.HistoryDatabase 10 | import com.mm.hamcompose.repository.HttpRepository 11 | import com.mm.hamcompose.repository.PagingArticle 12 | import com.mm.hamcompose.ui.page.base.BaseCollectViewModel 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class SquareViewModel @Inject constructor( 18 | private var repo: HttpRepository, 19 | private val db: HistoryDatabase, 20 | ): BaseCollectViewModel
(repo) { 21 | 22 | var pagingData = MutableLiveData(null) 23 | var isRefreshing = mutableStateOf(false) 24 | 25 | override fun start() { 26 | initThat { 27 | pagingData.value = squareData() 28 | } 29 | } 30 | 31 | fun refresh() { 32 | resetListIndex() 33 | isRefreshing.value = true 34 | pagingData.value = null 35 | pagingData.value = squareData() 36 | isRefreshing.value = pagingData.value==null 37 | } 38 | 39 | private fun squareData() = repo.getSquareData().cachedIn(viewModelScope) 40 | 41 | fun saveDataToHistory(article: Article) { 42 | cacheHistory(db, article) 43 | } 44 | 45 | override fun onCleared() { 46 | LogUtils.e("SquareViewModel ===> ViewModel执行onCleared()") 47 | super.onCleared() 48 | } 49 | 50 | 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/home/wenda/WenDaViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.home.wenda 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.paging.cachedIn 7 | import com.mm.hamcompose.data.bean.Article 8 | import com.mm.hamcompose.repository.HttpRepository 9 | import com.mm.hamcompose.repository.PagingArticle 10 | import com.mm.hamcompose.ui.page.base.BaseViewModel 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class WenDaViewModel @Inject constructor( 16 | private var repo: HttpRepository 17 | ): BaseViewModel
() { 18 | 19 | var pagingData = MutableLiveData(null) 20 | var isRefreshing = mutableStateOf(false) 21 | 22 | override fun start() { 23 | initThat { pagingData.value = squareData() } 24 | } 25 | 26 | fun refresh() { 27 | resetListIndex() 28 | isRefreshing.value = true 29 | pagingData.value = null 30 | pagingData.value = squareData() 31 | isRefreshing.value = pagingData.value==null 32 | } 33 | 34 | private fun squareData() = repo.getWendaData().cachedIn(viewModelScope) 35 | 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/profile/ProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.profile 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import com.mm.hamcompose.data.bean.Article 5 | import com.mm.hamcompose.data.bean.PointsBean 6 | import com.mm.hamcompose.data.bean.UserInfo 7 | import com.mm.hamcompose.data.db.user.UserInfoDatabase 8 | import com.mm.hamcompose.data.http.HttpResult 9 | import com.mm.hamcompose.repository.HttpRepository 10 | import com.mm.hamcompose.ui.page.base.BaseViewModel 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.flow.collectLatest 14 | import kotlinx.coroutines.flow.flow 15 | import kotlinx.coroutines.flow.flowOn 16 | import kotlinx.coroutines.withContext 17 | import javax.inject.Inject 18 | 19 | @HiltViewModel 20 | class ProfileViewModel @Inject constructor( 21 | private val repo: HttpRepository, 22 | private val db: UserInfoDatabase 23 | ) : BaseViewModel
() { 24 | 25 | val isLogin = mutableStateOf(false) 26 | val isRefresh = mutableStateOf(false) 27 | val messageCount = mutableStateOf(0) 28 | var userInfo = mutableStateOf(null) 29 | var page = mutableStateOf(1) 30 | var myPoints = mutableStateOf(null) 31 | val myArticles = mutableStateOf(mutableListOf
()) 32 | 33 | override fun start() { 34 | checkLoginState() 35 | } 36 | 37 | private fun initUserRemoteData() { 38 | initMessageCount() 39 | initBasicUserInfo() 40 | getMyShareArticles() 41 | } 42 | 43 | fun refresh() { 44 | isRefresh.value = true 45 | checkLoginState() 46 | } 47 | 48 | private fun initBasicUserInfo() { 49 | async { 50 | repo.getBasicUserInfo().collectLatest { response -> 51 | when (response) { 52 | is HttpResult.Success -> { 53 | myPoints.value = response.result.coinInfo 54 | userInfo.value = response.result.userInfo 55 | insertNewestUserInfo(response.result.userInfo) 56 | } 57 | is HttpResult.Error -> { 58 | println(response.exception.message) 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | private suspend fun insertNewestUserInfo(user: UserInfo) { 66 | withContext(Dispatchers.IO) { 67 | db.userInfoDao().insertUserInfo(user) 68 | } 69 | } 70 | 71 | private fun initMessageCount() { 72 | async { 73 | repo.getMessageCount().collectLatest { response -> 74 | when (response) { 75 | is HttpResult.Success -> { 76 | messageCount.value = response.result 77 | } 78 | is HttpResult.Error -> { 79 | println(response.exception.message) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | private fun checkLoginState() { 87 | async { 88 | flow { emit(db.userInfoDao().queryUserInfo()) } 89 | .flowOn(Dispatchers.IO) 90 | .collectLatest { users -> 91 | isLogin.value = users.isNotEmpty() 92 | if (users.isNotEmpty()) { 93 | userInfo.value = users[0] 94 | initUserRemoteData() 95 | } 96 | } 97 | } 98 | } 99 | 100 | private fun isNotInit() = userInfo.value==null && myPoints.value==null 101 | 102 | private fun getMyShareArticles() { 103 | async { 104 | repo.getMyShareArticles(page.value).collectLatest { response -> 105 | isRefresh.value = false 106 | when (response) { 107 | is HttpResult.Success -> { 108 | myPoints.value = response.result.coinInfo 109 | myArticles.value = response.result.shareArticles.datas 110 | } 111 | is HttpResult.Error -> { 112 | println(response.exception.message) 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | override fun onCleared() { 120 | super.onCleared() 121 | println("ProfileViewModel ==> onClear") 122 | } 123 | 124 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/profile/history/HistoryPage.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.profile.history 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.lazy.LazyColumn 9 | import androidx.compose.foundation.lazy.items 10 | import androidx.compose.foundation.lazy.itemsIndexed 11 | import androidx.compose.material.ScaffoldState 12 | import androidx.compose.material.icons.Icons 13 | import androidx.compose.material.icons.filled.Info 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.rememberCoroutineScope 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.unit.dp 20 | import androidx.hilt.navigation.compose.hiltViewModel 21 | import androidx.navigation.NavHostController 22 | import com.mm.hamcompose.data.bean.WebData 23 | import com.mm.hamcompose.ui.route.RouteName 24 | import com.mm.hamcompose.ui.route.RouteUtils 25 | import com.mm.hamcompose.ui.route.RouteUtils.back 26 | import com.mm.hamcompose.ui.widget.* 27 | 28 | @OptIn(ExperimentalFoundationApi::class) 29 | @Composable 30 | fun HistoryPage( 31 | navCtrl: NavHostController, 32 | scaffoldState: ScaffoldState, 33 | viewModel: HistoryViewModel = hiltViewModel() 34 | ) { 35 | 36 | viewModel.start() 37 | val historyList by remember { viewModel.list } 38 | val isClear by remember { viewModel.isClear } 39 | val asyncScope = rememberCoroutineScope() 40 | 41 | if (isClear) { 42 | popupSnackBar(asyncScope, scaffoldState, SNACK_INFO, "历史记录已清空") 43 | viewModel.isClear.value = false 44 | } 45 | 46 | Column( 47 | modifier = Modifier.fillMaxSize() 48 | ) { 49 | HamToolBar( 50 | title = "历史浏览记录", 51 | rightText = "清除所有", 52 | onBack = { navCtrl.back() }, 53 | onRightClick = { 54 | viewModel.clearAllHistory() 55 | } 56 | ) 57 | ListTitle(title = "最近在看", modifier = Modifier.padding(top = 12.dp, bottom = 5.dp)) 58 | if (historyList.isNotEmpty()) { 59 | LazyColumn { 60 | itemsIndexed(historyList) { index, item -> 61 | TextContent( 62 | text = "${index + 1}. ${item.title}", 63 | modifier = Modifier 64 | .padding(horizontal = 10.dp, vertical = 5.dp) 65 | .clickable { 66 | RouteUtils.navTo( 67 | navCtrl = navCtrl, 68 | destinationName = RouteName.WEB_VIEW, 69 | args = WebData(item.title, item.link) 70 | ) 71 | }, 72 | maxLines = 2, 73 | ) 74 | } 75 | 76 | } 77 | } else { 78 | EmptyView(tips = "暂无浏览记录", imageVector = Icons.Default.Info) 79 | } 80 | 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/profile/history/HistoryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.profile.history 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import com.mm.hamcompose.data.bean.HistoryRecord 5 | import com.mm.hamcompose.data.db.history.HistoryDatabase 6 | import com.mm.hamcompose.ui.page.base.BaseViewModel 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.withContext 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class HistoryViewModel @Inject constructor(private val db: HistoryDatabase): BaseViewModel() { 14 | 15 | var isClear = mutableStateOf(false) 16 | 17 | override fun start() { 18 | initThat { getHistoryList() } 19 | } 20 | 21 | private fun getHistoryList() { 22 | async { 23 | val history = withContext(Dispatchers.IO) { db.historyDao().queryAll() } 24 | withContext(Dispatchers.Main) { 25 | list.value = history.toMutableList() 26 | } 27 | } 28 | } 29 | 30 | fun clearAllHistory() { 31 | async { 32 | withContext(Dispatchers.IO) { db.historyDao().deleteAll() } 33 | withContext(Dispatchers.Main) { 34 | list.value = mutableListOf() 35 | isClear.value = true 36 | } 37 | } 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/profile/message/MessagePage.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.profile.message 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.lazy.LazyColumn 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.Refresh 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.unit.dp 9 | import androidx.hilt.navigation.compose.hiltViewModel 10 | import androidx.navigation.NavHostController 11 | import androidx.paging.compose.LazyPagingItems 12 | import androidx.paging.compose.collectAsLazyPagingItems 13 | import com.google.accompanist.pager.ExperimentalPagerApi 14 | import com.google.accompanist.pager.HorizontalPager 15 | import com.google.accompanist.pager.rememberPagerState 16 | import com.mm.hamcompose.ui.route.RouteUtils.back 17 | import com.mm.hamcompose.ui.widget.EmptyView 18 | import com.mm.hamcompose.ui.widget.HamToolBar 19 | import com.mm.hamcompose.ui.widget.SwitchTabBar 20 | import kotlinx.coroutines.launch 21 | 22 | @OptIn(ExperimentalPagerApi::class) 23 | @Composable 24 | fun MessagePage( 25 | navCtrl: NavHostController, 26 | viewModel: MessageViewModel = hiltViewModel() 27 | ) { 28 | 29 | val titles by remember { viewModel.titles } 30 | val tabIndex by remember { viewModel.tabIndex } 31 | val unreadMessages = viewModel.pagingUnread.value?.collectAsLazyPagingItems() 32 | val readedMessages = viewModel.pagingReaded.value?.collectAsLazyPagingItems() 33 | 34 | Column { 35 | 36 | HamToolBar(title = "我的消息", onBack = { navCtrl.back() }) 37 | 38 | val coroutineScope = rememberCoroutineScope() 39 | val pagerState = rememberPagerState( 40 | pageCount = titles.size, 41 | initialPage = tabIndex, 42 | initialOffscreenLimit = titles.size 43 | ) 44 | 45 | SwitchTabBar( 46 | titles = titles, 47 | selectIndex = tabIndex, 48 | ) { 49 | viewModel.tabIndex.value = it 50 | coroutineScope.launch { 51 | pagerState.scrollToPage(tabIndex) 52 | } 53 | } 54 | 55 | HorizontalPager(state = pagerState) { page -> 56 | viewModel.tabIndex.value = pagerState.currentPage 57 | when (page) { 58 | 0 -> MessageScreen(unreadMessages, false) { 59 | viewModel.refreshUnreadData() 60 | } 61 | 1 -> MessageScreen(readedMessages, true) { 62 | viewModel.refreshReadedData() 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | @Composable 70 | private fun MessageScreen(data: LazyPagingItems?, isReaded: Boolean, onRefresh: ()-> Unit) { 71 | if (data == null) { 72 | EmptyView( 73 | tips = if (isReaded) "没有已读消息" else "没有未读消息", 74 | imageVector = Icons.Default.Refresh, 75 | onClick = onRefresh) 76 | } else { 77 | LazyColumn { 78 | //TODO 未知消息的数据json 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/profile/message/MessageViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.profile.message 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.paging.cachedIn 7 | import com.mm.hamcompose.data.bean.TabTitle 8 | import com.mm.hamcompose.repository.HttpRepository 9 | import com.mm.hamcompose.repository.PagingAny 10 | import com.mm.hamcompose.ui.page.base.BaseViewModel 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class MessageViewModel @Inject constructor(private val repo: HttpRepository): BaseViewModel() { 16 | 17 | 18 | var tabIndex = mutableStateOf(0) 19 | var errorMessage = mutableStateOf(null) 20 | var pagingUnread = MutableLiveData(null) 21 | var pagingReaded = MutableLiveData(null) 22 | val titles = mutableStateOf(mutableListOf( 23 | TabTitle(401, "未读消息"), 24 | TabTitle(402, "已读消息"), 25 | )) 26 | 27 | 28 | override fun start() { 29 | initThat { 30 | pagingUnread.value = unread() 31 | pagingReaded.value = readed() 32 | } 33 | } 34 | 35 | fun refreshUnreadData() { 36 | pagingUnread.value = null 37 | pagingUnread.value = unread() 38 | } 39 | 40 | fun refreshReadedData() { 41 | pagingReaded.value = null 42 | pagingReaded.value = readed() 43 | } 44 | 45 | private fun unread() = repo.getUnreadMessages().cachedIn(viewModelScope) 46 | 47 | private fun readed() = repo.getReadedMessages().cachedIn(viewModelScope) 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/profile/points/PointsRankingViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.profile.points 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.paging.cachedIn 7 | import com.mm.hamcompose.data.bean.PointsBean 8 | import com.mm.hamcompose.data.bean.TabTitle 9 | import com.mm.hamcompose.data.http.HttpResult 10 | import com.mm.hamcompose.repository.HttpRepository 11 | import com.mm.hamcompose.repository.PagingPoints 12 | import com.mm.hamcompose.ui.page.base.BaseViewModel 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import kotlinx.coroutines.flow.collectLatest 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class PointsRankingViewModel @Inject constructor( 19 | private val repo: HttpRepository, 20 | ) : BaseViewModel() { 21 | 22 | val pagingRanking = MutableLiveData(null) 23 | val pagingRecords = MutableLiveData(null) 24 | val personalPoints = mutableStateOf(null) 25 | var tabIndex = mutableStateOf(0) 26 | var errorMessage = mutableStateOf(null) 27 | val titles = mutableStateOf( 28 | mutableListOf( 29 | TabTitle(501, "排行榜"), 30 | TabTitle(502, "我的积分"), 31 | ) 32 | ) 33 | 34 | override fun start() { 35 | initThat { 36 | fetchData() 37 | } 38 | } 39 | 40 | private fun fetchData() { 41 | if (personalPoints.value == null) { 42 | requestPersonPoints() 43 | } 44 | if (pagingRanking.value == null) { 45 | pagingRanking.value = ranking() 46 | } 47 | if (pagingRecords.value == null) { 48 | pagingRecords.value = records() 49 | } 50 | } 51 | 52 | private fun requestPersonPoints() { 53 | async { 54 | repo.getMyPointsRanking() 55 | .collectLatest { response -> 56 | when (response) { 57 | is HttpResult.Success -> { 58 | personalPoints.value = response.result 59 | } 60 | is HttpResult.Error -> { 61 | errorMessage.value = response.exception.message 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | private fun ranking(): PagingPoints { 69 | return repo.getPointsRankings().cachedIn(viewModelScope) 70 | } 71 | 72 | 73 | private fun records() = repo.getPointsRecords().cachedIn(viewModelScope) 74 | 75 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/profile/settings/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.profile.settings 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.compose.ui.graphics.Color 5 | import com.blankj.utilcode.util.SPUtils 6 | import com.mm.hamcompose.data.bean.UserInfo 7 | import com.mm.hamcompose.data.db.user.UserInfoDatabase 8 | import com.mm.hamcompose.data.http.HttpResult 9 | import com.mm.hamcompose.data.store.DataStoreUtils 10 | import com.mm.hamcompose.repository.HttpRepository 11 | import com.mm.hamcompose.theme.THEME_COLOR_KEY 12 | import com.mm.hamcompose.ui.page.base.BaseViewModel 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.delay 16 | import kotlinx.coroutines.flow.collectLatest 17 | import kotlinx.coroutines.withContext 18 | import javax.inject.Inject 19 | 20 | @HiltViewModel 21 | class SettingsViewModel @Inject constructor( 22 | private val repo: HttpRepository, 23 | private val db: UserInfoDatabase 24 | ): BaseViewModel() { 25 | 26 | val cacheSize = mutableStateOf(0) 27 | 28 | var logout = mutableStateOf(false) 29 | var themeIndex = mutableStateOf(SPUtils.getInstance().getInt(THEME_COLOR_KEY, 0)) 30 | var selectTheme = mutableStateOf(null) 31 | 32 | 33 | override fun start() { 34 | initThat { } 35 | } 36 | 37 | fun logout() { 38 | async { 39 | repo.logout().collectLatest { 40 | when(it) { 41 | is HttpResult.Success -> { } 42 | is HttpResult.Error -> { 43 | //退出登录的情况下,不走success判断分支 44 | val nullNotice = "the result of remote's request is null" 45 | if (it.exception.message==nullNotice) { 46 | clearUserInfo() 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | private fun clearUserInfo() { 55 | async { 56 | withContext(Dispatchers.IO) { 57 | db.userInfoDao().deleteAllUserInfo() 58 | DataStoreUtils.clear() 59 | delay(10) 60 | logout.value = true 61 | } 62 | } 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/profile/sharer/SharerViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.profile.sharer 2 | 3 | import androidx.compose.runtime.mutableStateListOf 4 | import androidx.compose.runtime.mutableStateOf 5 | import com.mm.hamcompose.data.bean.Article 6 | import com.mm.hamcompose.data.bean.MY_USER_ID 7 | import com.mm.hamcompose.data.bean.PointsBean 8 | import com.mm.hamcompose.data.http.HttpResult 9 | import com.mm.hamcompose.repository.HttpRepository 10 | import com.mm.hamcompose.ui.page.base.BaseViewModel 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.flow.collectLatest 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class SharerViewModel @Inject constructor(private val repo: HttpRepository): BaseViewModel
() { 17 | 18 | private var page = mutableStateOf(1) 19 | var points = mutableStateOf(null) 20 | var articles = mutableStateListOf
() 21 | private val userId = mutableStateOf(-1) 22 | var isLoadingMore = mutableStateOf(false) 23 | var hasMore = mutableStateOf(false) 24 | var errorMessage = mutableStateOf("") 25 | 26 | fun setupUserId(id: Int) { 27 | this.userId.value = id 28 | } 29 | 30 | override fun start() { 31 | initThat { 32 | startLoading() 33 | getShareData() 34 | } 35 | } 36 | 37 | fun nextPage() { 38 | if (hasMore.value) { 39 | page.value += 1 40 | getShareData() 41 | } 42 | } 43 | 44 | private fun getShareData() { 45 | async { 46 | isLoadingMore.value = true 47 | val call = if (userId.value == MY_USER_ID) { 48 | repo.getMyShareArticles(page.value) 49 | } else { 50 | repo.getAuthorShareArticles(userId.value, page.value) 51 | } 52 | call.collectLatest { response -> 53 | when (response) { 54 | is HttpResult.Success -> { 55 | 56 | points.value = response.result.coinInfo 57 | response.result.shareArticles.datas.run { 58 | if (!isNullOrEmpty()) { 59 | articles.addAll(this) 60 | } 61 | errorMessage.value = if (isNullOrEmpty()) "啥都没有~" else "" 62 | hasMore.value = !response.result.shareArticles.over && size >= 0 63 | } 64 | 65 | } 66 | is HttpResult.Error -> { 67 | println(response.exception.message) 68 | errorMessage.value = response.exception.message ?: "未知异常" 69 | } 70 | } 71 | resetStatus() 72 | } 73 | } 74 | } 75 | 76 | private fun resetStatus() { 77 | stopLoading() 78 | isLoadingMore.value = false 79 | } 80 | 81 | 82 | fun deleteMyArticle(id: Int) { 83 | async { 84 | repo.deleteMyShareArticle(id).collectLatest { response -> 85 | when (response) { 86 | is HttpResult.Success -> { 87 | } 88 | is HttpResult.Error -> { 89 | println(response.exception.message) 90 | val error = response.exception.message 91 | if (error == "the result of remote's request is null") { 92 | val deleteItem = articles.find { it.id == id } 93 | articles.remove(deleteItem) 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/main/profile/user/UserViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.main.profile.user 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import com.blankj.utilcode.util.LogUtils 5 | import com.mm.hamcompose.data.bean.UserInfo 6 | import com.mm.hamcompose.data.http.HttpResult 7 | import com.mm.hamcompose.repository.HttpRepository 8 | import com.mm.hamcompose.data.db.user.UserInfoDatabase 9 | import com.mm.hamcompose.ui.page.base.BaseViewModel 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.collectLatest 13 | import kotlinx.coroutines.withContext 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class UserViewModel @Inject constructor( 18 | private val repo: HttpRepository, 19 | private val userDb: UserInfoDatabase 20 | ) : BaseViewModel() { 21 | 22 | var errorMessage = mutableStateOf(null) 23 | var isRegister = mutableStateOf(false) 24 | var isLogin = mutableStateOf(false) 25 | 26 | override fun start() {} 27 | 28 | fun register(account: String, password: String, repassword: String) { 29 | async { 30 | repo.register(account, password, repassword) 31 | .collectLatest { response -> 32 | when (response) { 33 | is HttpResult.Success -> { 34 | isRegister.value = true 35 | } 36 | is HttpResult.Error -> { 37 | errorMessage.value = response.exception.message 38 | //ToastUtils.showShort(errorMessage.value) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | fun login(account: String, password: String) { 46 | async { 47 | repo.login(account, password) 48 | .collectLatest { response -> 49 | when (response) { 50 | is HttpResult.Success -> { 51 | saveUserInfo(response.result) 52 | isLogin.value = true 53 | } 54 | is HttpResult.Error -> { 55 | errorMessage.value = response.exception.message 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | override fun onCleared() { 63 | super.onCleared() 64 | LogUtils.e("invoke onCleared of ViewModel") 65 | } 66 | 67 | private fun saveUserInfo(userInfo: UserInfo) { 68 | async { 69 | withContext(Dispatchers.IO) { 70 | userDb.userInfoDao().insertUserInfo(userInfo) 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/webview/WebView.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.webview 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.res.ColorStateList 5 | import android.view.ViewGroup 6 | import android.webkit.WebView 7 | import android.widget.FrameLayout 8 | import android.widget.ProgressBar 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.runtime.* 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.viewinterop.AndroidView 15 | import androidx.navigation.NavHostController 16 | import com.blankj.utilcode.util.SizeUtils 17 | import com.google.accompanist.swiperefresh.rememberSwipeRefreshState 18 | import com.mm.hamcompose.R 19 | import com.mm.hamcompose.data.bean.WebData 20 | import com.mm.hamcompose.theme.ToolBarHeight 21 | import com.mm.hamcompose.ui.route.RouteUtils.back 22 | import com.mm.hamcompose.ui.widget.HamToolBar 23 | 24 | @SuppressLint("UseCompatLoadingForDrawables") 25 | @Composable 26 | fun WebViewPage( 27 | webData: WebData, 28 | navCtrl: NavHostController 29 | ) { 30 | var ctrl: WebViewCtrl? by remember { mutableStateOf(null) } 31 | Box { 32 | var isRefreshing: Boolean by remember { mutableStateOf(false) } 33 | val refreshState = rememberSwipeRefreshState(isRefreshing) 34 | AndroidView( 35 | modifier = Modifier 36 | .padding(top = ToolBarHeight) 37 | .fillMaxSize(), 38 | factory = { context -> 39 | FrameLayout(context).apply { 40 | layoutParams = FrameLayout.LayoutParams( 41 | FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT 42 | ) 43 | val progressView = ProgressBar(context).apply { 44 | layoutParams = ViewGroup.LayoutParams( 45 | ViewGroup.LayoutParams.MATCH_PARENT, 46 | SizeUtils.dp2px(2f) 47 | ) 48 | progressDrawable = 49 | context.resources.getDrawable(R.drawable.horizontal_progressbar) 50 | indeterminateTintList = 51 | ColorStateList.valueOf(context.resources.getColor(R.color.teal_200)) 52 | } 53 | val webView = WebView(context).apply { 54 | layoutParams = ViewGroup.LayoutParams( 55 | ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT 56 | ) 57 | } 58 | addView(webView) 59 | addView(progressView) 60 | ctrl = WebViewCtrl(this, webData.url, onWebCall = { isFinish -> 61 | isRefreshing = !isFinish 62 | }) 63 | ctrl?.initSettings() 64 | } 65 | 66 | }, 67 | update = { 68 | 69 | } 70 | ) 71 | 72 | HamToolBar(title = webData.title ?: "标题", onBack = { 73 | ctrl?.onDestroy() 74 | navCtrl.back() 75 | }) 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/page/webview/WebViewCtrl.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.page.webview 2 | 3 | import android.graphics.Bitmap 4 | import android.net.http.SslError 5 | import android.os.Build 6 | import android.view.View 7 | import android.webkit.* 8 | import android.widget.FrameLayout 9 | import android.widget.ProgressBar 10 | import androidx.annotation.RequiresApi 11 | 12 | class WebViewCtrl( 13 | private val mView: FrameLayout, 14 | private var linkUrl: String, 15 | private val onWebCall: (isFinish: Boolean) -> Unit 16 | ) { 17 | 18 | private val webView by lazy { mView.getChildAt(0) as WebView } 19 | private val progressBar by lazy { mView.getChildAt(1) as ProgressBar } 20 | 21 | fun initSettings() { 22 | onWebCall(false) 23 | setWebSettings() 24 | setupWebClient() 25 | } 26 | 27 | fun onDestroy() { 28 | mView.removeAllViews() 29 | webView.destroy() 30 | } 31 | 32 | private fun setWebSettings() { 33 | val webSettings = webView.settings 34 | //如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript 35 | webSettings.javaScriptEnabled = false 36 | //设置自适应屏幕,两者合用 37 | webSettings.useWideViewPort = true //将图片调整到适合webview的大小 38 | webSettings.loadWithOverviewMode = true // 缩放至屏幕的大小 39 | //缩放操作 40 | webSettings.setSupportZoom(true) //支持缩放,默认为true。是下面那个的前提。 41 | webSettings.builtInZoomControls = true //设置内置的缩放控件。若为false,则该WebView不可缩放 42 | webSettings.displayZoomControls = false //隐藏原生的缩放控件 43 | 44 | //其他细节操作 45 | webSettings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK //关闭webview中缓存 46 | webSettings.allowFileAccess = true //设置可以访问文件 47 | webSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口 48 | webSettings.loadsImagesAutomatically = true //支持自动加载图片 49 | webSettings.defaultTextEncodingName = "UTF-8"//设置编码格式 50 | } 51 | 52 | 53 | private fun setupWebClient() { 54 | webView.webViewClient = NewWebViewClient() 55 | webView.webChromeClient = ProgressWebViewChromeClient() 56 | refresh() 57 | } 58 | 59 | fun refresh() { 60 | webView.loadUrl(linkUrl) 61 | } 62 | 63 | 64 | inner class ProgressWebViewChromeClient : WebChromeClient() { 65 | override fun onProgressChanged(view: WebView?, newProgress: Int) { 66 | super.onProgressChanged(view, newProgress) 67 | progressBar.progress = newProgress 68 | } 69 | 70 | override fun onReceivedTitle(view: WebView?, title: String?) { 71 | super.onReceivedTitle(view, title) 72 | } 73 | } 74 | 75 | 76 | inner class NewWebViewClient : WebViewClient() { 77 | 78 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 79 | override fun shouldOverrideUrlLoading( 80 | view: WebView?, 81 | request: WebResourceRequest? 82 | ): Boolean { 83 | linkUrl = request?.url.toString() 84 | return super.shouldOverrideUrlLoading(view, request) 85 | } 86 | 87 | override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { 88 | linkUrl = url?:"NullUrlString" 89 | return super.shouldOverrideUrlLoading(view, url) 90 | } 91 | 92 | override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { 93 | progressBar.visibility = View.VISIBLE 94 | super.onPageStarted(view, url, favicon) 95 | } 96 | 97 | override fun onPageFinished(view: WebView?, url: String?) { 98 | progressBar.visibility = View.GONE 99 | onWebCall(true) 100 | super.onPageFinished(view, url) 101 | } 102 | 103 | override fun onReceivedSslError( 104 | view: WebView?, 105 | handler: SslErrorHandler?, 106 | error: SslError? 107 | ) { 108 | handler?.proceed() 109 | } 110 | } 111 | 112 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/route/BottomNavRoute.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.route 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.* 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | import com.mm.hamcompose.R 8 | 9 | sealed class BottomNavRoute( 10 | var routeName: String, 11 | @StringRes var stringId: Int, 12 | var icon: ImageVector 13 | ) { 14 | object Home: BottomNavRoute(RouteName.HOME, R.string.home, Icons.Default.Home) 15 | object Category: BottomNavRoute(RouteName.CATEGORY, R.string.category, Icons.Default.Menu) 16 | object Collection: BottomNavRoute(RouteName.COLLECTION, R.string.collection, Icons.Default.Favorite) 17 | object Profile: BottomNavRoute(RouteName.PROFILE, R.string.profile, Icons.Default.Person) 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/route/RouteName.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.route 2 | 3 | object RouteName { 4 | const val HOME = "home" 5 | const val CATEGORY = "category" 6 | const val COLLECTION = "collection" 7 | const val PROFILE = "profile" 8 | 9 | const val STRUCTURE_LIST = "structure_list" 10 | const val ARTICLE_SEARCH = "article_search" 11 | const val PUB_ACCOUNT_DETAIL = "pub_account_detail" 12 | const val PUB_ACCOUNT_SEARCH = "pub_account_search" 13 | const val WEB_VIEW = "web_view" 14 | 15 | const val LOGIN = "login" 16 | const val REGISTER = "register" 17 | const val RANKING = "ranking" 18 | const val MESSAGE = "message" 19 | const val SETTINGS = "settings" 20 | const val EDIT_WEBSITE = "edit_website" 21 | const val SHARER = "sharer" 22 | const val SHARE_ARTICLE = "share_article" 23 | const val HISTORY = "history" 24 | 25 | const val GIRL_PHOTO = "girl_photo" 26 | const val GIRL_INFO = "girl_info" 27 | 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/route/RouteUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.route 2 | 3 | import android.os.Bundle 4 | import android.os.Parcelable 5 | import androidx.navigation.NavGraph.Companion.findStartDestination 6 | import androidx.navigation.NavHostController 7 | 8 | /** 9 | * 路由名称 10 | */ 11 | object RouteUtils { 12 | 13 | const val STEAD_SYMBOL = "^0^" 14 | 15 | //初始化Bundle参数 16 | fun initBundle(params: Parcelable) = Bundle().apply { putParcelable(ARGS, params) } 17 | 18 | /** 19 | * 导航到某个页面 20 | */ 21 | fun navTo( 22 | navCtrl: NavHostController, 23 | destinationName: String, 24 | args: Any? = null, 25 | backStackRouteName: String? = null, 26 | isLaunchSingleTop: Boolean = true, 27 | needToRestoreState: Boolean = true, 28 | ) { 29 | 30 | var singleArgument = "" 31 | if (args!=null) { 32 | when(args) { 33 | is Parcelable -> { 34 | navCtrl.currentBackStackEntry?.replaceArguments(initBundle(args)) 35 | } 36 | is String -> { 37 | singleArgument = String.format("/%s", args) 38 | } 39 | is Int -> { 40 | singleArgument = String.format("/%s", args) 41 | } 42 | is Float -> { 43 | singleArgument = String.format("/%s", args) 44 | } 45 | is Double -> { 46 | singleArgument = String.format("/%s", args) 47 | } 48 | is Boolean -> { 49 | singleArgument = String.format("/%s", args) 50 | } 51 | is Long -> { 52 | singleArgument = String.format("/%s", args) 53 | } 54 | } 55 | } else { 56 | navCtrl.previousBackStackEntry?.arguments = null 57 | navCtrl.currentBackStackEntry?.arguments = null 58 | } 59 | println("导航到: $destinationName") 60 | navCtrl.navigate("$destinationName$singleArgument") { 61 | if (backStackRouteName != null) { 62 | popUpTo(backStackRouteName) { saveState = true } 63 | } 64 | launchSingleTop = isLaunchSingleTop 65 | restoreState = needToRestoreState 66 | } 67 | } 68 | 69 | fun NavHostController.back() { 70 | navigateUp() 71 | } 72 | 73 | private fun getPopUpId(navCtrl: NavHostController, routeName: String?): Int { 74 | val defaultId = navCtrl.graph.findStartDestination().id 75 | return if (routeName == null) { 76 | defaultId 77 | } else { 78 | navCtrl.findDestination(routeName)?.id ?: defaultId 79 | } 80 | } 81 | 82 | fun getArguments(navCtrl: NavHostController): T? { 83 | return navCtrl.previousBackStackEntry?.arguments?.getParcelable(ARGS) 84 | } 85 | 86 | /** 87 | * 各个序列化的参数类的key名 88 | */ 89 | private const val ARGS = "args" 90 | 91 | 92 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/widget/Animations.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.widget 2 | 3 | import androidx.compose.animation.* 4 | import androidx.compose.animation.AnimatedVisibility 5 | import androidx.compose.runtime.Composable 6 | 7 | @OptIn(ExperimentalAnimationApi::class) 8 | @Composable 9 | fun FadeAnim( 10 | isVisible: Boolean, 11 | content: @Composable ()-> Unit 12 | ) { 13 | AnimatedVisibility( 14 | visible = isVisible, 15 | enter = fadeIn(), 16 | exit = fadeOut() 17 | ) { 18 | content 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/widget/AsyncImage.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.widget 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.layout.ContentScale 7 | import coil.compose.rememberImagePainter 8 | import com.google.accompanist.placeholder.material.placeholder 9 | import com.mm.hamcompose.R 10 | import com.mm.hamcompose.theme.HamTheme 11 | 12 | @Composable 13 | fun NetworkImage( 14 | url: String, 15 | isLoading: Boolean = true, 16 | contentDesc: String? = null, 17 | contentScale: ContentScale = ContentScale.Crop, 18 | modifier: Modifier = Modifier, 19 | ) { 20 | Image( 21 | painter = rememberImagePainter( 22 | data = url, 23 | builder = { 24 | crossfade(true) 25 | placeholder(R.drawable.no_banner) 26 | } 27 | ), 28 | contentDescription = contentDesc, 29 | contentScale = ContentScale.FillBounds, 30 | modifier = modifier 31 | .placeholder( 32 | visible = isLoading, 33 | color = HamTheme.colors.placeholder 34 | ) 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/widget/Buttons.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.widget 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.combinedClickable 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.text.style.TextAlign 19 | import androidx.compose.ui.text.style.TextOverflow 20 | import androidx.compose.ui.unit.Dp 21 | import androidx.compose.ui.unit.dp 22 | import androidx.compose.ui.unit.sp 23 | import com.google.accompanist.placeholder.material.placeholder 24 | import com.mm.hamcompose.theme.HamTheme 25 | import com.mm.hamcompose.theme.buttonCorner 26 | import com.mm.hamcompose.theme.buttonHeight 27 | import com.mm.hamcompose.theme.white1 28 | import org.jetbrains.annotations.NotNull 29 | 30 | 31 | @Composable 32 | fun HamButton( 33 | text: String, 34 | modifier: Modifier = Modifier, 35 | bgColor: Color = HamTheme.colors.secondBtnBg, 36 | textColor: Color = HamTheme.colors.textPrimary, 37 | onClick: () -> Unit 38 | ) { 39 | Box( 40 | modifier = modifier 41 | .fillMaxWidth() 42 | .height(buttonHeight) 43 | .background(color = bgColor, shape = RoundedCornerShape(buttonCorner)) 44 | .clickable { 45 | onClick() 46 | } 47 | ) { 48 | TextContent(text = text, color = textColor, modifier = Modifier.align(Alignment.Center)) 49 | } 50 | } 51 | 52 | 53 | @Composable 54 | fun PrimaryButton( 55 | text: String, 56 | modifier: Modifier = Modifier, 57 | onClick: () -> Unit 58 | ) { 59 | HamButton( 60 | text = text, 61 | modifier = modifier, 62 | textColor = HamTheme.colors.textPrimary, 63 | onClick = onClick, 64 | bgColor = HamTheme.colors.themeUi 65 | ) 66 | } 67 | 68 | @Composable 69 | fun SecondlyButton( 70 | text: String, 71 | modifier: Modifier = Modifier, 72 | onClick: () -> Unit 73 | ) { 74 | HamButton( 75 | text = text, 76 | modifier = modifier, 77 | textColor = HamTheme.colors.textSecondary, 78 | onClick = onClick 79 | ) 80 | } 81 | 82 | @OptIn(ExperimentalFoundationApi::class) 83 | @Composable 84 | fun LabelTextButton( 85 | @NotNull text: String, 86 | modifier: Modifier = Modifier, 87 | isSelect: Boolean = true, 88 | specTextColor: Color? = null, 89 | cornerValue: Dp = 25.dp / 2, 90 | isLoading: Boolean = false, 91 | onClick: (() -> Unit)? = null, 92 | onLongClick: (() -> Unit)? = null 93 | ) { 94 | Text( 95 | text = text, 96 | modifier = modifier 97 | .height(25.dp) 98 | .clip(shape = RoundedCornerShape(cornerValue)) 99 | .background( 100 | color = if (isSelect && !isLoading) HamTheme.colors.themeUi else HamTheme.colors.secondBtnBg, 101 | ) 102 | .padding( 103 | horizontal = 10.dp, 104 | vertical = 3.dp 105 | ) 106 | .combinedClickable( 107 | enabled = !isLoading, 108 | onClick = { onClick?.invoke() }, 109 | onLongClick = { onLongClick?.invoke() } 110 | ) 111 | .placeholder( 112 | visible = isLoading, 113 | color = HamTheme.colors.placeholder 114 | ), 115 | fontSize = 13.sp, 116 | textAlign = TextAlign.Center, 117 | color = specTextColor ?: if (isSelect) white1 else HamTheme.colors.textSecondary, 118 | overflow = TextOverflow.Ellipsis, 119 | maxLines = 1, 120 | ) 121 | } 122 | 123 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/widget/SnackBar.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.widget 2 | 3 | import androidx.compose.material.ScaffoldState 4 | import androidx.compose.material.Snackbar 5 | import androidx.compose.material.SnackbarData 6 | import androidx.compose.runtime.Composable 7 | import com.mm.hamcompose.theme.HamTheme 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.delay 10 | import kotlinx.coroutines.launch 11 | 12 | const val SNACK_INFO = "" 13 | const val SNACK_WARN = " " 14 | const val SNACK_ERROR = " " 15 | const val SNACK_SUCCESS = "OK" 16 | 17 | @Composable 18 | fun HamSnackBar(data: SnackbarData) { 19 | Snackbar( 20 | snackbarData = data, 21 | backgroundColor = when (data.actionLabel) { 22 | SNACK_INFO -> HamTheme.colors.themeUi 23 | SNACK_WARN -> HamTheme.colors.warn 24 | SNACK_ERROR -> HamTheme.colors.error 25 | SNACK_SUCCESS -> HamTheme.colors.success 26 | else -> HamTheme.colors.themeUi 27 | }, 28 | actionColor = HamTheme.colors.textPrimary, 29 | contentColor = HamTheme.colors.textPrimary, 30 | ) 31 | } 32 | 33 | fun popupSnackBar( 34 | scope: CoroutineScope, 35 | scaffoldState: ScaffoldState, 36 | label: String, 37 | message: String, 38 | onDismissCallback: () -> Unit = {} 39 | ) { 40 | scope.launch { 41 | scaffoldState.snackbarHostState.showSnackbar(actionLabel = label, message = message) 42 | onDismissCallback.invoke() 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/ui/widget/Title.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.ui.widget 2 | 3 | import androidx.compose.foundation.text.selection.SelectionContainer 4 | import androidx.compose.material.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.text.font.FontWeight 9 | import androidx.compose.ui.text.style.TextAlign 10 | import androidx.compose.ui.text.style.TextOverflow 11 | import androidx.compose.ui.unit.TextUnit 12 | import com.google.accompanist.placeholder.material.placeholder 13 | import com.mm.hamcompose.theme.* 14 | 15 | @Composable 16 | fun LargeTitle( 17 | title: String, 18 | modifier: Modifier = Modifier, 19 | color: Color? = null, 20 | isLoading: Boolean = false 21 | ) { 22 | Title( 23 | title = title, 24 | modifier = modifier, 25 | fontSize = H3, 26 | color = color ?: HamTheme.colors.textPrimary, 27 | fontWeight = FontWeight.Bold, 28 | isLoading = isLoading 29 | ) 30 | } 31 | 32 | @Composable 33 | fun MainTitle( 34 | title: String, 35 | modifier: Modifier = Modifier, 36 | maxLine: Int = 1, 37 | textAlign: TextAlign = TextAlign.Start, 38 | color: Color = HamTheme.colors.textPrimary, 39 | isLoading: Boolean = false 40 | ) { 41 | Title( 42 | title = title, 43 | modifier = modifier, 44 | fontSize = H4, 45 | color = color, 46 | fontWeight = FontWeight.SemiBold, 47 | maxLine = maxLine, 48 | textAlign = textAlign, 49 | isLoading = isLoading 50 | ) 51 | } 52 | 53 | @Composable 54 | fun MediumTitle( 55 | title: String, 56 | modifier: Modifier = Modifier, 57 | color: Color = HamTheme.colors.textPrimary, 58 | textAlign: TextAlign = TextAlign.Start, 59 | isLoading: Boolean = false 60 | ) { 61 | Title( 62 | title = title, 63 | fontSize = H5, 64 | modifier = modifier, 65 | color = color, 66 | textAlign = textAlign, 67 | isLoading = isLoading 68 | ) 69 | } 70 | 71 | @Composable 72 | fun TextContent( 73 | text: String, 74 | modifier: Modifier = Modifier, 75 | color: Color = HamTheme.colors.textSecondary, 76 | maxLines: Int = 99, 77 | textAlign: TextAlign = TextAlign.Start, 78 | canCopy: Boolean = false, 79 | isLoading: Boolean = false 80 | ) { 81 | if (canCopy) { 82 | SelectionContainer { 83 | Title( 84 | title = text, 85 | modifier = modifier, 86 | fontSize = H6, 87 | color = color, 88 | maxLine = maxLines, 89 | textAlign = textAlign, 90 | isLoading = isLoading 91 | ) 92 | } 93 | } else { 94 | Title( 95 | title = text, 96 | modifier = modifier, 97 | fontSize = H6, 98 | color = color, 99 | maxLine = maxLines, 100 | textAlign = textAlign, 101 | isLoading = isLoading 102 | ) 103 | } 104 | 105 | } 106 | 107 | @Composable 108 | fun MiniTitle( 109 | text: String, 110 | modifier: Modifier = Modifier, 111 | color: Color = HamTheme.colors.textSecondary, 112 | maxLines: Int = 1, 113 | textAlign: TextAlign = TextAlign.Start, 114 | isLoading: Boolean = false 115 | ) { 116 | Title( 117 | title = text, 118 | modifier = modifier, 119 | fontSize = H7, 120 | color = color, 121 | maxLine = maxLines, 122 | textAlign = textAlign, 123 | isLoading = isLoading, 124 | ) 125 | } 126 | 127 | @Composable 128 | fun Title( 129 | title: String, 130 | modifier: Modifier = Modifier, 131 | fontSize: TextUnit, 132 | color: Color = HamTheme.colors.textSecondary, 133 | fontWeight: FontWeight = FontWeight.Normal, 134 | maxLine: Int = 1, 135 | textAlign: TextAlign = TextAlign.Start, 136 | isLoading: Boolean = false 137 | ) { 138 | Text( 139 | text = title, 140 | modifier = modifier 141 | .placeholder( 142 | visible = isLoading, 143 | color = HamTheme.colors.placeholder 144 | ), 145 | fontSize = fontSize, 146 | color = color, 147 | maxLines = maxLine, 148 | overflow = TextOverflow.Ellipsis, 149 | textAlign = textAlign 150 | ) 151 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/util/CacheDataManager.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.util 2 | 3 | import android.content.Context 4 | import android.os.Environment 5 | import java.io.File 6 | import java.math.BigDecimal 7 | 8 | object CacheDataManager { 9 | 10 | /** 11 | * 获取App缓存大小 12 | */ 13 | fun getTotalCacheSize(context: Context): String { 14 | 15 | var cacheSize = getFolderSize(context.cacheDir) 16 | if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { 17 | cacheSize += getFolderSize(context.externalCacheDir) 18 | } 19 | return getFormatSize(cacheSize.toDouble()) 20 | } 21 | 22 | /** 23 | * 清理缓存 24 | */ 25 | fun clearAllCache(context: Context): Boolean { 26 | with(context) { 27 | deleteDir(cacheDir) 28 | if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { 29 | if (externalCacheDir == null) { 30 | println("清理缓存失败") 31 | } 32 | return false 33 | } 34 | 35 | if (externalCacheDir != null) { 36 | return deleteFile(externalCacheDir?.absolutePath) 37 | } 38 | return false 39 | } 40 | } 41 | 42 | } 43 | 44 | private fun deleteDir(dir: File): Boolean { 45 | if (dir.isDirectory) { 46 | val children = dir.list() 47 | for (i in children.indices) { 48 | val success = deleteDir(File(dir, children[i])) 49 | if (!success) { 50 | return false 51 | } 52 | } 53 | } 54 | return dir.delete() 55 | } 56 | 57 | /** 58 | * 获取文件 59 | * Context.getExternalFilesDir() --> SDCard/Android/data/你的应用的包名/files/ 60 | * 目录,一般放一些长时间保存的数据 61 | * Context.getExternalCacheDir() --> 62 | * SDCard/Android/data/你的应用包名/cache/目录,一般存放临时缓存数据 63 | */ 64 | fun getFolderSize(file: File?): Long { 65 | var size: Long = 0 66 | file?.run { 67 | try { 68 | val fileList = listFiles() 69 | for (i in fileList.indices) { 70 | // 如果下面还有文件 71 | size += if (fileList[i].isDirectory) { 72 | getFolderSize(fileList[i]) 73 | } else { 74 | fileList[i].length() 75 | } 76 | } 77 | } catch (e: Exception) { 78 | e.printStackTrace() 79 | } 80 | } 81 | return size 82 | } 83 | 84 | /** 85 | * 格式化单位 86 | */ 87 | fun getFormatSize(size: Double): String { 88 | 89 | val kiloByte = size / 1024 90 | if (kiloByte < 1) { 91 | return size.toString() + "Byte" 92 | } 93 | 94 | val megaByte = kiloByte / 1024 95 | 96 | if (megaByte < 1) { 97 | val result1 = BigDecimal(kiloByte.toString()) 98 | return result1.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "KB" 99 | } 100 | 101 | val gigaByte = megaByte / 1024 102 | 103 | if (gigaByte < 1) { 104 | val result2 = BigDecimal(megaByte.toString()) 105 | return result2.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "MB" 106 | } 107 | 108 | val teraBytes = gigaByte / 1024 109 | 110 | if (teraBytes < 1) { 111 | val result3 = BigDecimal(gigaByte.toString()) 112 | return result3.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "GB" 113 | } 114 | 115 | val result4 = BigDecimal(teraBytes) 116 | return result4.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "TB" 117 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/util/Navigation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.mm.hamcompose.util 18 | 19 | import android.os.Parcelable 20 | import androidx.activity.OnBackPressedCallback 21 | import androidx.activity.OnBackPressedDispatcher 22 | import androidx.compose.runtime.saveable.listSaver 23 | import androidx.compose.runtime.toMutableStateList 24 | 25 | /** 26 | * A simple navigator which maintains a back stack. 27 | */ 28 | class Navigator private constructor( 29 | initialBackStack: List, 30 | backDispatcher: OnBackPressedDispatcher 31 | ) { 32 | constructor( 33 | initial: T, 34 | backDispatcher: OnBackPressedDispatcher 35 | ) : this(listOf(initial), backDispatcher) 36 | 37 | //回退栈 38 | private val backStack = initialBackStack.toMutableStateList() 39 | //回退栈的回调 40 | private val backCallback = object : OnBackPressedCallback(canGoBack()) { 41 | override fun handleOnBackPressed() { 42 | back() 43 | } 44 | }.also { callback -> 45 | backDispatcher.addCallback(callback) 46 | } 47 | //栈顶 48 | val current: T get() = backStack.last() 49 | 50 | // 51 | fun back() { 52 | backStack.removeAt(backStack.lastIndex) 53 | backCallback.isEnabled = canGoBack() 54 | } 55 | 56 | fun navigate(destination: T) { 57 | backStack += destination 58 | backCallback.isEnabled = canGoBack() 59 | } 60 | 61 | private fun canGoBack(): Boolean = backStack.size > 1 62 | 63 | companion object { 64 | /** 65 | * Serialize the back stack to save to instance state. 66 | */ 67 | fun saver(backDispatcher: OnBackPressedDispatcher) = 68 | listSaver, T>( 69 | save = { navigator -> navigator.backStack.toList() }, 70 | restore = { backstack -> Navigator(backstack, backDispatcher) } 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/mm/hamcompose/util/RegexUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mm.hamcompose.util 2 | 3 | import java.text.SimpleDateFormat 4 | 5 | class RegexUtils { 6 | 7 | fun symbolClear(text: String?): String { 8 | // val pattern = Pattern.compile() 9 | if (text.isNullOrEmpty()) { 10 | return "" 11 | } 12 | val regex = Regex("<[a-z]+>||<[a-z]+/>") 13 | if (text.contains(regex)) { 14 | return text.replace(regex, "") 15 | } 16 | return text 17 | } 18 | 19 | fun timestamp(time: String?): String? { 20 | time ?: return null 21 | return kotlin.runCatching { 22 | SimpleDateFormat("yyyy-MM-dd HH:mm").parse(time) 23 | time.substring(0, time.indexOf(" ")) 24 | }.getOrDefault(time) 25 | } 26 | } -------------------------------------------------------------------------------- /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/horizontal_progressbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_more.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_article.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_author.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_camera.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_community.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_data.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_drawer.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_exit_app.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_feedback.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_help.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_history_record.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_hot.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_settings.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_welfare.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_message.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_ranking.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_border.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_theme.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_time.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_back.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_back_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/no_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/drawable/no_banner.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/wukong.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/drawable/wukong.jpeg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/splash_image01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-xhdpi/splash_image01.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/splash_image02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-xhdpi/splash_image02.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/splash_image03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-xhdpi/splash_image03.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/splash_image04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-xhdpi/splash_image04.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/splash_image05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-xhdpi/splash_image05.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manqianzhuang/HamApp/edfd54f8cf8f3b319751d450fdf79f8c4abf5a42/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #00BFFF 4 | #BB86FC 5 | #6200EE 6 | #3700B3 7 | #03DAC5 8 | #018786 9 | #000000 10 | #FFFFFF 11 | #00000000 12 | #cdcdcd 13 | #bfbfbf 14 | #8a8a8a 15 | #FEFEFE 16 | #FAFAFA 17 | #F8F8F8 18 | #DBDBDB 19 | #e6e6e6 20 | #2c2c2c 21 | 22 | #FFDEAD 23 | #0000CD 24 | #FF69B4 25 | #8B4513 26 | #FF7F00 27 | #FFA500 28 | #FFD700 29 | #FFFF00 30 | #FF3030 31 | #90EE90 32 | #00FF7F 33 | #F0FFFF 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | HamCompose 3 | WebView 4 | MainActivity 5 | 首页 6 | 分类 7 | 收藏 8 | 我的 9 | 标题 10 | 作者 11 | 网站 12 | 链接 13 | 用户 14 | 保存 15 | 登录 16 | 注册 17 | 请输入标题 18 | 请输入名称 19 | 请输入内容 20 | 请输入用户名 21 | 请输入帐号 22 | 请输入密码 23 | 请再次输入密码 24 | 请输入邀请码 25 | 请输入网址 26 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 23 | 24 |