This commit is contained in:
coco
2026-07-03 16:23:31 +08:00
commit 7a4fb0e6ae
1979 changed files with 101570 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
+40
View File
@@ -0,0 +1,40 @@
# WanAndroidCompose版本
#### 介绍
此WanAndroid app客户端项目使用Android官方的Jetpack Compose完成,
遵循MVVM架构思路,以下为本项目用到的框架:
jetpack compose, viewModel, retrofit, okhttp3, coroutine/flow, paging3
room, accompanist, hilt, gson, glide/picasso, navigation.
项目模块:
首页(推荐、广场、项目、问答),
分类(体系、导航、公众号,分享文章),
收藏(网址、文章),
我的(我的文章、积分排行、历史浏览、添加文章、设置、消息、主题色、清缓存等)
登录、登出、注册
#### 软件架构
Mvvm, Composable + viewModel + repository
#### ScreenShot
https://github.com/manqianzhuang/HamApp/tree/origin/screenshot
#### 关于项目
1. 项目地址: https://github.com/manqianzhuang/HamApp.git
2. apk地址: https://www.pgyer.com/F9NX
3. 联系方式: ganzhuangman@gmail.com
4. API提供: 鸿洋(WanAndroid开放api
#### 使用说明
1. 此项目仅提供学习用途,未经允许不得用于商业项目
2. 感谢鸿洋大佬提供的WanAndroid网站,让我们可以学习到很多的android/flutter/前端等技术
3. 欢迎各位提PR,我会抽时间不断优化代码和修复bug。如有请教,请邮件联系
#### TODO
1. 添加动画 = WAIT
2. 我的消息开发 = WAIT
+1
View File
@@ -0,0 +1 @@
/build
+151
View File
@@ -0,0 +1,151 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "kotlin-kapt"
id "dagger.hilt.android.plugin"
id "kotlin-parcelize"
id "androidx.navigation.safeargs.kotlin"
}
android {
compileSdk = androidCompileSdk
buildToolsVersion = androidBuildToolsVersion
defaultConfig {
applicationId = "com.mm.hamcompose"
minSdk = project.minSdkVersion
targetSdk = project.targetSdkVersion
versionCode = project.currentVersionCode
versionName = project.currentVersionName
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
multiDexEnabled true
}
buildTypes {
release {
minifyEnabled false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
useIR = true
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = composeVersion
kotlinCompilerVersion = "1.4.30"
}
}
dependencies {
implementation "androidx.core:core-ktx:$coreKtxVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "com.google.android.material:material:$materialVersion"
implementation "androidx.activity:activity-compose:$activityComposeVersion"
/** accompanist辅助插件 */
implementation "com.google.accompanist:accompanist-insets:$accompanistVersion"
implementation "com.google.accompanist:accompanist-coil:0.15.0"
//系统ui控制器
implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion"
//glide
implementation "com.google.accompanist:accompanist-glide:0.15.0"
//viewPager
implementation "com.google.accompanist:accompanist-pager:$accompanistVersion"
implementation "com.google.accompanist:accompanist-pager-indicators:$accompanistVersion"
//下拉刷新
implementation "com.google.accompanist:accompanist-swiperefresh:$accompanistVersion"
//流式布局
implementation "com.google.accompanist:accompanist-flowlayout:$accompanistVersion"
//placeholder
implementation "com.google.accompanist:accompanist-placeholder-material:$accompanistVersion"
//implementation "com.google.accompanist:accompanist-placeholder:$accompanistVersion"
//约束布局
implementation "androidx.constraintlayout:constraintlayout-compose:$constraintComposeVersion"
//compose插件
implementation "androidx.compose.ui:ui:$composeVersion"
implementation "androidx.compose.ui:ui-tooling:$composeVersion"
implementation "androidx.compose.ui:ui-util:$composeVersion"
implementation "androidx.compose.material:material:$composeVersion"
implementation "androidx.compose.foundation:foundation:$composeVersion"
implementation "androidx.compose.foundation:foundation-layout:$composeVersion"
implementation "androidx.compose.runtime:runtime-livedata:$composeVersion"
//lifecycle插件
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$viewModelComposeVersion"
//协程
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$coroutineAdapterVersion"
//hilt inject framework
implementation "com.google.dagger:hilt-android:$hiltVersion"
kapt "com.google.dagger:hilt-compiler:$hiltVersion"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hiltVersion"
kaptAndroidTest "com.google.dagger:hilt-compiler:$hiltVersion"
testImplementation "com.google.dagger:hilt-android-testing:$hiltVersion"
kaptTest "com.google.dagger:hilt-compiler:$hiltVersion"
implementation "androidx.hilt:hilt-navigation-compose:$hiltComposeVersion"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hiltComposeVersion"
kapt "androidx.hilt:hilt-compiler:$hiltCompilerVersion"
//http request
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.google.code.gson:gson:$gsonVersion"
//util
implementation "com.blankj:utilcode:$blankjToolVersion"
//测试
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$extJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$composeVersion"
//glide
implementation "com.github.bumptech.glide:glide:$glideVersion"
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
//picasso
implementation "com.squareup.picasso:picasso:$picassoVersion"
//paging分页库
implementation "androidx.paging:paging-runtime:$pagingVersion"
testImplementation "androidx.paging:paging-common:$pagingVersion"
implementation "androidx.paging:paging-compose:$pagingComposeVersion"
//room数据库
implementation "androidx.room:room-runtime:$roomVersion"
implementation "androidx.room:room-ktx:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
implementation "androidx.room:room-paging:$roomPagingVersion"
implementation "androidx.navigation:navigation-compose:$navigationComposeVersion"
//数据保存,用于cookie持久化
implementation "androidx.datastore:datastore-preferences:$datastoreVersion"
implementation "androidx.datastore:datastore-core:$datastoreVersion"
implementation "org.jetbrains.kotlin:kotlin-reflect:1.5.21"
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.mm.hamcompose
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.mm.hamcompose", appContext.packageName)
}
}
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.mm.hamcompose">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".HamApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.HamCompose">
<activity
android:name=".ui.HomeActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.HamCompose.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,29 @@
package com.mm.hamcompose
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import com.mm.hamcompose.data.store.DataStoreUtils
import dagger.hilt.android.HiltAndroidApp
/**
* 1. 所有使用 Hilt 的 App 必须包含 一个使用 @HiltAndroidApp 注解的 Application
* 2. @HiltAndroidApp 将会触发 Hilt 代码的生成,包括用作应用程序依赖项容器的基类
* 3. 生成的 Hilt 组件依附于 Application 的生命周期,它也是 App 的父组件,提供其他组件访问的依赖
* 4. 在 Application 中设置好 @HiltAndroidApp 之后,就可以使用 Hilt 提供的组件了,
* Hilt 提供的 @AndroidEntryPoint 注解用于提供 Android 类的依赖(Activity、Fragment、View、Service、BroadcastReceiver)等等
* Application 使用 @HiltAndroidApp 注解
*/
@HiltAndroidApp
class HamApp: Application() {
companion object {
@SuppressLint("StaticFieldLeak")
lateinit var CONTEXT: Context
}
override fun onCreate() {
super.onCreate()
CONTEXT = this
DataStoreUtils.init(this)
}
}
@@ -0,0 +1,106 @@
package com.mm.hamcompose
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
@Preview
@Composable
fun IconsPreview() {
LazyColumn(
Modifier
.fillMaxWidth()
.height(800.dp)
) {
item {
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.ArrowBack, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.AccountBox, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.AccountCircle, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.AddCircle, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.ArrowDropDown, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.ArrowForward, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.Build, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.Clear, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Call, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Check, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.CheckCircle, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Close, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Create, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.DateRange, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Done, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.Edit, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Email, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.ExitToApp, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.Face, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Favorite, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.FavoriteBorder, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.Home, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.Info, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.KeyboardArrowDown, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.KeyboardArrowLeft, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.KeyboardArrowRight, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.KeyboardArrowUp, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.List, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.LocationOn, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.MoreVert, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.MailOutline, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Menu, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.Person, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Phone, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Place, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.PlayArrow, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Send, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Share, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.ShoppingCart, contentDescription = null, modifier = Modifier.padding(end=10.dp))
Icon(Icons.Default.Star, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.ThumbUp, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
FlowRow(Modifier.padding(bottom = 20.dp)) {
Icon(Icons.Default.Warning, contentDescription = null, modifier = Modifier.padding(end=10.dp))
}
}
}
}
@@ -0,0 +1,35 @@
package com.mm.hamcompose.data.bean
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.mm.hamcompose.data.db.DbConst
const val MY_USER_ID = -999
data class MenuTitle(
val title: String,
val iconRes: Int?
)
data class TabTitle(
val id: Int,
val text: String,
var cachePosition: Int = 0,
var selected: Boolean = false
)
@Entity(tableName = DbConst.history)
data class HistoryRecord(
@PrimaryKey var id: Int,
var title: String,
var link: String,
var niceDate: String,
var shareUser: String,
var userId: Int,
var author: String,
var superChapterId: Int,
var superChapterName: String,
var chapterId: Int,
var chapterName: String,
var desc: String,
)
@@ -0,0 +1,233 @@
package com.mm.hamcompose.data.bean
import android.os.Parcelable
import androidx.room.*
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import com.mm.hamcompose.data.db.DbConst
import com.mm.hamcompose.theme.HamTheme
import kotlinx.parcelize.Parcelize
import kotlin.reflect.javaType
import kotlin.reflect.typeOf
data class HomeThemeBean(
var theme: HamTheme.Theme
)
data class BasicBean<T>(
var data: T?,
var errorCode: Int,
var errorMsg: String
)
data class ListWrapper<T>(
var curPage: Int,
var offset: Int,
var over: Boolean,
var pageCount: Int,
var size: Int,
var total: Int,
var datas: ArrayList<T>
)
data class BannerBean(
var desc: String?,
var id: Int,
var imagePath: String?,
var isVisible: Int,
var order: Int,
var title: String?,
var type: Int,
var url: String?
)
data class SharerBean<T>(
val coinInfo: PointsBean,
val shareArticles: ListWrapper<T>
)
data class BasicUserInfo(
val coinInfo: PointsBean,
val userInfo: UserInfo
)
@Parcelize
data class ParentBean(
var children: MutableList<ParentBean>?,
var courseId: Int = -1,
var id: Int = -1,
var name: String? = "分类",
var order: Int = -1,
var parentChapterId: Int = -1,
var userControlSetTop: Boolean = false,
var visible: Int = -1,
var icon: String? = null,
var link: String? = null
): Parcelable
data class NaviWrapper(
var articles: MutableList<Article>?,
var cid: Int,
var name: String?
)
@Parcelize
data class Article(
var apkLink: String? = "",
var audit: Int = -1,
var author: String? = "作者",
var canEdit: Boolean = false,
var chapterId: Int = -1,
var chapterName: String? = "章节",
var collect: Boolean = false,
var courseId: Int = -1,
var desc: String? = "描述",
var descMd: String? = "描述Md",
var envelopePic: String? = "图片1",
var fresh: Boolean = false,
var host: String? = "https://www.wanandroid.com",
var id: Int = -1,
var link: String? = "https://www.wanandroid.com",
var niceDate: String? = "1970-0-0",
var niceShareDate: String? = "1970-0-0",
var origin: String? = "",
var prefix: String? = "",
var projectLink: String? = "https://www.wanandroid.com",
var publishTime: Long = 0L,
var realSuperChapterId: Int = -1,
var selfVisible: Int = -1,
var shareDate: Long = 0L,
var shareUser: String? = "分享者",
var superChapterId: Int = -1,
var superChapterName: String? = "超级分类",
var tags: MutableList<ArticleTag>? = null,
var title: String? = "标题",
var type: Int = -1,
var userId: Int = -1,
var visible: Int = -1,
var zan: Int = -1
): Parcelable
@Parcelize
data class ArticleTag(
var name: String,
var url:String
): Parcelable
@Entity(tableName = DbConst.hotKey)
data class Hotkey(
@PrimaryKey(autoGenerate = true) var id: Int = 0,
var link: String?,
var name: String?,
var order: Int,
var visible: Int
)
@Parcelize
data class WebData(
var title: String?,
var url: String
): Parcelable
@Entity(tableName = DbConst.userInfo)
@TypeConverters(IntTypeConverter::class)
@Parcelize
data class UserInfo(
@PrimaryKey var id: Int,
var admin: Boolean,
var chapterTops: MutableList<Int>,
var coinCount: Int,
var collectIds: MutableList<Int>,
var email: String,
var icon: String,
var nickname: String,
var password: String,
var token: String,
var type: Int,
var username: String,
): Parcelable
object IntTypeConverter {
@OptIn(ExperimentalStdlibApi::class)
@TypeConverter
fun fromJson(value: String): List<Int> {
return Gson().fromJson(value, typeOf<MutableList<Int>>().javaType)
}
@TypeConverter
fun toJson(json: MutableList<Int>): String {
return Gson().toJson(json)
}
}
data class PointsBean(
var id: Int?,
var coinCount: String,
var level: Int?,
var nickname: String,
var rank: String?,
var userId: Int,
var username: String,
var date: String?,
var desc: String?,
var reason: String?,
var type: Int?,
)
data class CollectBean(
var author: String = "",
var chapterId: Int = 0,
var chapterName: String = "",
var courseId: Int = 0,
var desc: String = "",
var envelopePic: String = "",
var id: Int = 0,
var link: String = "",
var niceDate: String = "",
var origin: String = "",
var originId: Int = 0,
var publishTime: Long = 0,
var title: String = "",
var userId: Int = 0,
var visible: Int = 0,
var zan: Int = 0
)
/********************* Begin: Welfare Data ***********************/
abstract class GankBasedBean<T : Any?> {
val page: Int = 1
@SerializedName("page_count") val pageSize: Int = 20
val status: Int = 100
@SerializedName("total_counts") val totalSize: Int = 96
abstract var data: T?
}
data class WelfareBean(
override var data: List<WelfareData>?
) : GankBasedBean<List<WelfareData>>()
@Parcelize
data class WelfareData(
@SerializedName("_id") var id: String?,
var author: String?,
var category: String?,
var createAt: String?,
var desc: String?,
var images: MutableList<String>,
var likeCounts: Int,
var publishedAt: String?,
var stars: Int,
var title: String?,
var type: String?,
var url: String?,
var views: Int,
) : Parcelable
/********************* Begin: Welfare Data ***********************/
@@ -0,0 +1,11 @@
package com.mm.hamcompose.data.db
object DbConst {
const val dbVersion = 1
const val hotKeyDbName = "hot_key_db"
const val userDbName = "user_db"
const val historyDbName = "history_db"
const val hotKey = "hot_key"
const val userInfo = "user_info"
const val history = "history"
}
@@ -0,0 +1,21 @@
package com.mm.hamcompose.data.db.history
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.mm.hamcompose.data.bean.HistoryRecord
@Dao
interface HistoryDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertHistory(vararg history: HistoryRecord)
@Query("SELECT * FROM history")
suspend fun queryAll(): Array<HistoryRecord>
@Query("DELETE FROM history")
suspend fun deleteAll()
}
@@ -0,0 +1,11 @@
package com.mm.hamcompose.data.db.history
import androidx.room.Database
import androidx.room.RoomDatabase
import com.mm.hamcompose.data.bean.HistoryRecord
import com.mm.hamcompose.data.db.DbConst
@Database(entities = [HistoryRecord::class], version = DbConst.dbVersion)
abstract class HistoryDatabase: RoomDatabase() {
abstract fun historyDao(): HistoryDao
}
@@ -0,0 +1,11 @@
package com.mm.hamcompose.data.db.hotkey
import androidx.room.Database
import androidx.room.RoomDatabase
import com.mm.hamcompose.data.bean.Hotkey
import com.mm.hamcompose.data.db.DbConst
@Database(entities = [Hotkey::class], version = DbConst.dbVersion)
abstract class HotkeyDatabase: RoomDatabase() {
abstract fun hotkeyDao(): HotkeysDao
}
@@ -0,0 +1,24 @@
package com.mm.hamcompose.data.db.hotkey
import androidx.room.*
import com.mm.hamcompose.data.bean.Hotkey
@Dao
interface HotkeysDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertHotkeys(vararg keys: Hotkey)
@Update
suspend fun updateHotkeys(vararg keys: Hotkey)
@Delete
suspend fun deleteKeys(vararg keys: Hotkey)
@Query("SELECT * FROM hot_key")
suspend fun loadAllKeys(): Array<Hotkey>
@Query("DELETE FROM hot_key")
suspend fun deleteAll()
}
@@ -0,0 +1,28 @@
package com.mm.hamcompose.data.db.user
import androidx.room.*
import com.mm.hamcompose.data.bean.UserInfo
import com.mm.hamcompose.data.db.DbConst
import retrofit2.http.DELETE
@Dao
interface UserInfoDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUserInfo(userInfo: UserInfo)
@Update
suspend fun updateUserInfo(userInfo: UserInfo)
@Query("SELECT * FROM user_info")
suspend fun queryUserInfo(): List<UserInfo?>
@Delete(entity = UserInfo::class)
suspend fun deleteUserInfo(vararg userInfo: UserInfo): Int
@Query("DELETE FROM user_info")
suspend fun deleteAllUserInfo()
}
@@ -0,0 +1,14 @@
package com.mm.hamcompose.data.db.user
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.mm.hamcompose.data.bean.IntTypeConverter
import com.mm.hamcompose.data.bean.UserInfo
import com.mm.hamcompose.data.db.DbConst
@Database(entities = [UserInfo::class], version = DbConst.dbVersion)
@TypeConverters(IntTypeConverter::class)
abstract class UserInfoDatabase: RoomDatabase() {
abstract fun userInfoDao(): UserInfoDao
}
@@ -0,0 +1,88 @@
package com.mm.hamcompose.data.http
import android.annotation.SuppressLint
import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
import com.mm.hamcompose.data.http.interceptor.CacheCookieInterceptor
import com.mm.hamcompose.data.http.interceptor.LogInterceptor
import com.mm.hamcompose.data.http.interceptor.SetCookieInterceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.net.ssl.*
/**
* Created by Superman. 19/5/27
*/
object ApiCall {
/**
* 请求超时时间
*/
private const val DEFAULT_TIMEOUT = 30000
private lateinit var SERVICE: HttpService
//手动创建一个OkHttpClient并设置超时时间
val retrofit: HttpService
get() {
if (!ApiCall::SERVICE.isInitialized) {
SERVICE = Retrofit.Builder()
.client(okHttp)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory.invoke())
.baseUrl(HttpService.url)
.build()
.create(HttpService::class.java)
}
return SERVICE
}
//手动创建一个OkHttpClient并设置超时时间
val okHttp: OkHttpClient
get() {
return OkHttpClient.Builder().run {
connectTimeout(DEFAULT_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
readTimeout(DEFAULT_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
writeTimeout(DEFAULT_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
addInterceptor(SetCookieInterceptor())
addInterceptor(CacheCookieInterceptor())
addInterceptor(LogInterceptor())
//不验证证书
sslSocketFactory(createSSLSocketFactory())
hostnameVerifier(TrustAllNameVerifier())
build()
}
}
private fun createSSLSocketFactory(): SSLSocketFactory {
lateinit var ssfFactory: SSLSocketFactory
try {
val sslFactory = SSLContext.getInstance("TLS")
sslFactory.init(null, arrayOf(TrustAllCerts()), SecureRandom());
ssfFactory = sslFactory.socketFactory
} catch (e: Exception) {
print("SSL错误:${e.message}")
}
return ssfFactory
}
}
class TrustAllNameVerifier: HostnameVerifier {
@SuppressLint("BadHostnameVerifier")
override fun verify(hostname: String?, session: SSLSession?): Boolean = true
}
@SuppressLint("CustomX509TrustManager")
class TrustAllCerts : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
@SuppressLint("TrustAllX509TrustManager")
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}
@@ -0,0 +1,10 @@
package com.mm.hamcompose.data.http
import java.lang.Exception
sealed class HttpResult<out T> {
data class Success<T>(val result: T): HttpResult<T>()
data class Error(val exception: Exception): HttpResult<Nothing>()
}
@@ -0,0 +1,245 @@
package com.mm.hamcompose.data.http
import com.mm.hamcompose.data.bean.*
import retrofit2.http.*
/**
* 网络请求接口
* 注意:接口前无需加斜杠
* create by Mqz at 4/19
*/
interface HttpService {
companion object {
const val url = "https://www.wanandroid.com"
}
//首页
@GET("/article/list/{page}/json")
suspend fun getIndexList(@Path("page") page: Int): BasicBean<ListWrapper<Article>>
//广场
@GET("/user_article/list/{page}/json")
suspend fun getSquareData(@Path("page") page: Int): BasicBean<ListWrapper<Article>>
//问答
@GET("/wenda/list/{page}/json")
suspend fun getWendaData(@Path("page") page: Int): BasicBean<ListWrapper<Article>>
//置顶文章
@GET("/article/top/json")
suspend fun getTopArticles(): BasicBean<MutableList<Article>>
//搜索热词
@GET("/hotkey/json")
suspend fun getHotkeys(): BasicBean<MutableList<Hotkey>>
//banner
@GET("/banner/json")
suspend fun getBanners(): BasicBean<MutableList<BannerBean>>
//体系
@GET("/tree/json")
suspend fun getStructureList(): BasicBean<MutableList<ParentBean>>
//导航
@GET("/navi/json")
suspend fun getNavigationList(): BasicBean<MutableList<NaviWrapper>>
//公众号列表
@GET("/wxarticle/chapters/json")
suspend fun getPublicInformation(): BasicBean<MutableList<ParentBean>>
//某作者的公众号文章列表
@GET("/wxarticle/list/{id}/{page}/json")
suspend fun getPublicArticles(
@Path("id") publicId: Int,
@Path("page") page: Int
): BasicBean<ListWrapper<Article>>
//某公众号作者的文章下搜索
@GET("/wxarticle/list/{id}/{page}/json")
suspend fun getPublicArticlesWithKey(
@Path("id") publicId: Int,
@Path("page") page: Int,
@Query("k") key: String
): BasicBean<ListWrapper<Article>>
//项目分类
@GET("/project/tree/json")
suspend fun getProjectCategory(): BasicBean<MutableList<ParentBean>>
//某个项目分类下的列表
@GET("/project/list/{page}/json")
suspend fun getProjects(
@Path("page") page: Int,
@Query("cid") cid: Int
): BasicBean<ListWrapper<Article>>
//某个体系下的文章列表
@GET("/article/list/{page}/json")
suspend fun getStructureArticles(
@Path("page") page: Int,
@Query("cid") cid: Int
): BasicBean<ListWrapper<Article>>
//在某个体系下搜索某作者的文章
@GET("/article/list/{page}/json")
suspend fun getArticlesByAuthor(
@Path("page") page: Int,
@Query("author") author: String
): BasicBean<ListWrapper<Article>>
//热门项目
@GET("/article/listproject/{page}/json")
suspend fun getHotProjects(@Path("page") page: Int): BasicBean<ListWrapper<Article>>
//首页查询文章
@FormUrlEncoded
@POST("/article/query/{page}/json")
suspend fun queryArticle(
@Path("page") page: Int,
@Field("k") key: String
): BasicBean<ListWrapper<Article>>
//注册
@FormUrlEncoded
@POST("/user/register")
suspend fun register(
@Field("username") userName: String,
@Field("password") password: String,
@Field("repassword") repassword: String,
): BasicBean<UserInfo>
//登录
@FormUrlEncoded
@POST("/user/login")
suspend fun login(
@Field("username") userName: String,
@Field("password") password: String,
): BasicBean<UserInfo>
//退出登录
@GET("/user/logout/json")
suspend fun logout(): BasicBean<Any>
//排行榜
@GET("/coin/rank/{page}/json")
suspend fun getPointsRankings(@Path("page") page: Int): BasicBean<ListWrapper<PointsBean>>
//积分记录
@GET("/lg/coin/list/{page}/json")
suspend fun getPointsRecords(@Path("page") page: Int): BasicBean<ListWrapper<PointsBean>>
//我的积分
@GET("/lg/coin/userinfo/json")
suspend fun getMyPointsRanking(): BasicBean<PointsBean>
//消息数量
@GET("/message/lg/count_unread/json")
suspend fun getMessageCount(): BasicBean<Int>
//未读消息列表
@GET("/message/lg/unread_list/{page}/json")
suspend fun getUnreadMessage(@Path("page") page: Int): BasicBean<ListWrapper<Any>>
//已读消息列表
@GET("/message/lg/readed_list/{page}/json")
suspend fun getReadedMessage(@Path("page") page: Int): BasicBean<ListWrapper<Any>>
//收藏列表
@GET("/lg/collect/list/{page}/json")
suspend fun getCollectionList(@Path("page") page: Int): BasicBean<ListWrapper<CollectBean>>
//收藏的网站
@GET("/lg/collect/usertools/json")
suspend fun getCollectUrls(): BasicBean<MutableList<ParentBean>>
//收藏站内文章
@POST("/lg/collect/{id}/json")
suspend fun collectInnerArticle(@Path("id") id: Int): BasicBean<Any>
//取消收藏站内文章(在首页等列表里取消)
@POST("/lg/uncollect_originId/{id}/json")
suspend fun uncollectInnerArticle(@Path("id") id: Int): BasicBean<Any>
//取消收藏站内文章(在收藏列表里取消)
@POST("/lg/uncollect/{id}/json")
suspend fun uncollectArticleById(
@Path("id") id: Int,
@Query("originId") originId: Int
): BasicBean<Any>
//添加收藏网站
@FormUrlEncoded
@POST("/lg/collect/addtool/json")
suspend fun addNewWebsiteCollect(
@Field("name") title: String,
@Field("link") linkUrl: String
): BasicBean<ParentBean>
//添加站外文章
@FormUrlEncoded
@POST("/lg/collect/add/json")
suspend fun addNewArticleCollect(
@Field("title") title: String,
@Field("link") linkUrl: String,
@Field("author") author: String,
): BasicBean<CollectBean>
//删除收藏网站
@FormUrlEncoded
@POST("/lg/collect/deletetool/json")
suspend fun deleteWebsite(@Field("id") id: Int): BasicBean<Any>
//编辑收藏的网站
@FormUrlEncoded
@POST("/lg/collect/updatetool/json")
suspend fun editCollectWebsite(
@Field("id") id: Int,
@Field("name") title: String,
@Field("link") linkUrl: String,
): BasicBean<Any>
//自己分享的文章
@GET("/user/lg/private_articles/{page}/json")
suspend fun getMyShareArticles(
@Path("page") page: Int,
@Query("page_size") size: Int = 40,
): BasicBean<SharerBean<Article>>
//根据文章id删除自己分享的文章
@POST("/lg/user_article/delete/{id}/json")
suspend fun deleteMyShareArticle(@Path("id") id: Int): BasicBean<Any>
//分享文章
@FormUrlEncoded
@POST("/lg/user_article/add/json")
suspend fun addMyShareArticle(
@Field("title") title: String,
@Field("link") linkUrl: String,
@Field("shareUser") shareUser: String,
): BasicBean<Any>
//其他作者分享的文章
@GET("/user/{userId}/share_articles/{page}/json")
suspend fun getAuthorShareArticles(
@Path("userId") userId: Int,
@Path("page") page: Int,
@Query("page_size") size: Int = 40,
): BasicBean<SharerBean<Article>>
@GET("/user/lg/userinfo/json")
suspend fun getBasicUserInfo(): BasicBean<BasicUserInfo>
// 福利
@GET("https://gank.io/api/v2/data/category/{category}/type/{type}/page/{page}/count/{pageSize}")
suspend fun getWelfareList(
@Path(value = "category", encoded = false) category: String = "Girl",
@Path(value = "type", encoded = false) type: String = "Girl",
@Path("page") page: Int,
@Path("pageSize") pageCount: Int = 40
): WelfareBean
}
@@ -0,0 +1,57 @@
package com.mm.hamcompose.data.http.interceptor
import com.mm.hamcompose.data.store.DataStoreUtils
import okhttp3.Interceptor
import okhttp3.Response
class CacheCookieInterceptor: Interceptor {
private val loginUrl = "user/login"
private val registerUrl = "user/register"
private val SET_COOKIE_KEY = "set-cookie"
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val requestUrl = request.url().toString()
val domain = request.url().host()
if (aboutUser(requestUrl)) {
val cookies = response.headers(SET_COOKIE_KEY)
if (cookies.isNotEmpty()) {
//cookie可能有多个,都保存下来
DataStoreUtils.putSyncData(domain, encodeCookie(cookies))
}
}
return response
}
private fun aboutUser(url: String): Boolean = url.contains(loginUrl) or url.contains(registerUrl)
}
/**
* 整理cookie
*/
private fun encodeCookie(cookies: List<String>): String {
val sb = StringBuilder()
val set = HashSet<String>()
cookies
.map { cookie ->
cookie.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}
.forEach { it ->
it.filterNot { set.contains(it) }.forEach { set.add(it) }
}
val ite = set.iterator()
while (ite.hasNext()) {
val cookie = ite.next()
sb.append(cookie).append(";")
}
val last = sb.lastIndexOf(";")
if (sb.length - 1 == last) {
sb.deleteCharAt(last)
}
return sb.toString()
}
@@ -0,0 +1,134 @@
package com.mm.hamcompose.data.http.interceptor
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import okhttp3.*
import java.io.IOException
import java.net.URLDecoder
import java.util.Objects.isNull
import javax.inject.Inject
/**
* Created by Superman on 2021/1/29.
*/
class LogInterceptor @Inject constructor() : Interceptor {
private val logTag = "http ## "
@RequiresApi(Build.VERSION_CODES.N)
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response? {
val request = chain.request()
return kotlin.runCatching { chain.proceed(request) }
.onSuccess {
logRequest(request, chain.connection())
if (it.isSuccessful) {
logResponse(it)
} else {
logThat(ColorLevel.WARN(it.message()?:"未知异常"))
}
}
.onFailure {
logThat(ColorLevel.ERROR(it.message?:"未知异常"))
}
.getOrThrow()
}
private fun logResponse(response: Response) {
val strb = StringBuffer()
strb.appendLine("\r\n")
strb.appendLine("<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-")
var headerText = ""
response.headers().toMultimap().forEach { header->
headerText += "请求 Header:{${header.key}=${header.value}}\n"
}
strb.appendln(headerText)
kotlin.runCatching {
//peek类似于clone数据流,监视,窥探,不能直接用原来的body的string流数据作为日志,会消费掉io,所有这里是peek,监测
val peekBody: ResponseBody = response.peekBody(1024 * 1024)
strb.appendln(peekBody.string())
}.getOrNull()
strb.appendLine(
"<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<" +
"-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-"
)
logThat(ColorLevel.INFO(strb.toString()))
}
/**
* 打印日志
*/
private fun logThat(tempLevel: ColorLevel) {
when (tempLevel) {
is ColorLevel.VERBOSE -> Log.v(logTag, tempLevel.logText)
is ColorLevel.DEBUG -> Log.d(logTag, tempLevel.logText)
is ColorLevel.INFO -> Log.i(logTag, tempLevel.logText)
is ColorLevel.WARN -> Log.w(logTag, tempLevel.logText)
is ColorLevel.ERROR -> Log.e(logTag, tempLevel.logText)
}
}
private fun logRequest(request: Request, connection: Connection?) {
val strb = StringBuilder()
strb.appendLine("\r\n")
strb.appendLine(
"->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->"
)
logHeaders(strb, request, connection)
strb.appendLine("RequestBody:${request.body().toString()}")
logThat(ColorLevel.VERBOSE(strb.toString()))
}
private fun logHeaders(strb: StringBuilder, request: Request, connection: Connection?) {
logBasic(strb, request, connection)
var headerStr = ""
request.headers().toMultimap().forEach { header->
headerStr += "请求 Header:{${header.key}=${header.value}}\n"
}
strb.appendln(headerStr)
}
private fun logBasic(strb: StringBuilder, request: Request, connection: Connection?) {
strb.appendLine(
"请求 method${request.method()} url:${decodeUrlStr(request.url().toString())} tag:" +
"${request.tag()} protocol:${connection?.protocol() ?: Protocol.HTTP_1_1}"
)
}
/**
* 对于url编码的string解码
*/
private fun decodeUrlStr(url: String): String? {
return kotlin.runCatching {
URLDecoder.decode(url, "utf-8")
}.onFailure { it.printStackTrace() }.getOrNull()
}
/**
* 打印日志范围
*/
enum class LogLevel {
NONE,//不打印
BASIC,//只打印行首,请求/响应
HEADERS, //打印请求和响应的所有header
BODY //打印所有
}
/**
* Log颜色等级,应用于android Logcat分为 v、d、i、w、e
*/
sealed class ColorLevel {
data class VERBOSE(val logText: String): ColorLevel()
data class DEBUG(val logText: String): ColorLevel()
data class INFO(val logText: String): ColorLevel()
data class WARN(val logText: String): ColorLevel()
data class ERROR(val logText: String): ColorLevel()
}
}
@@ -0,0 +1,22 @@
package com.mm.hamcompose.data.http.interceptor
import com.mm.hamcompose.data.store.DataStoreUtils
import okhttp3.Interceptor
import okhttp3.Response
class SetCookieInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val builder = request.newBuilder()
val domain = request.url().host()
//获取domain内的cookie
if (domain.isNotEmpty()) {
val cookie: String = DataStoreUtils.readStringData(domain, "")
if (cookie.isNotEmpty()) {
builder.addHeader("Cookie", cookie)
}
}
return chain.proceed(builder.build())
}
}
@@ -0,0 +1,42 @@
package com.mm.hamcompose.data.http.paging
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.blankj.utilcode.util.LogUtils
import com.mm.hamcompose.data.bean.BasicBean
import com.mm.hamcompose.data.bean.ListWrapper
import com.mm.hamcompose.data.http.HttpResult
class BasePagingSource<T: Any> constructor(
private val callDataFromRemoteServer: suspend (page: Int)-> HttpResult<BasicBean<ListWrapper<T>>>
): PagingSource<Int, T>() {
private var page: Int = -1
override fun getRefreshKey(state: PagingState<Int, T>): Int? {
return state.anchorPosition?.let {
val anchorPage = state.closestPageToPosition(it)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
println("当前页 ${params.key}")
page = params.key ?: 0
return when (val response = callDataFromRemoteServer(page)) {
is HttpResult.Success -> {
val data = response.result.data
val hasNotNext = (data!!.datas.size < params.loadSize) && (data.over)
LoadResult.Page(
data = response.result.data!!.datas,
prevKey = if (page - 1 > 0) page - 1 else null,
nextKey = if (hasNotNext) null else page+1
)
}
is HttpResult.Error -> {
LogUtils.e("网络请求异常: ${response.exception.message}")
LoadResult.Error(response.exception)
}
}
}
}
@@ -0,0 +1,32 @@
package com.mm.hamcompose.data.http.paging
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.blankj.utilcode.util.LogUtils
import com.mm.hamcompose.data.bean.WelfareData
import com.mm.hamcompose.data.http.HttpService
import javax.inject.Inject
class GirlPhotoPagingSource @Inject constructor(
private val apiService: HttpService,
): PagingSource<Int, WelfareData>() {
override fun getRefreshKey(state: PagingState<Int, WelfareData>): Int? = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, WelfareData> {
return try {
LogUtils.e("currentPage= ${params.key}, size=${params.loadSize}")
val page = params.key?: 0
val response = apiService.getWelfareList("Girl", "Girl", page, params.loadSize)
val isNextPage = response.data!!.isNotEmpty()
LoadResult.Page(
data = response.data!!,
prevKey = if (page>0) page-1 else null,
nextKey = if (isNextPage) page+1 else null
)
} catch (e: Exception) {
LogUtils.e("网络请求异常: ${e.message}")
LoadResult.Error(e)
}
}
}
@@ -0,0 +1,20 @@
package com.mm.hamcompose.data.http.paging
import androidx.paging.PagingConfig
class PagingFactory {
val pagingConfig = PagingConfig(
// 每页显示的数据的大小
pageSize = 20,
//开启占位符
enablePlaceholders = true,
//预刷新的距离,距离最后一个 item 多远时加载数据
prefetchDistance = 4,
//初始化加载数量,默认为 pageSize * 3
initialLoadSize = 20
)
}
@@ -0,0 +1,304 @@
package com.mm.hamcompose.data.store
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import com.mm.hamcompose.data.store.DataStoreUtils.clear
import com.mm.hamcompose.data.store.DataStoreUtils.clearSync
import com.mm.hamcompose.data.store.DataStoreUtils.getData
import com.mm.hamcompose.data.store.DataStoreUtils.getSyncData
import com.mm.hamcompose.data.store.DataStoreUtils.putData
import com.mm.hamcompose.data.store.DataStoreUtils.putSyncData
import com.mm.hamcompose.data.store.DataStoreUtils.readBooleanData
import com.mm.hamcompose.data.store.DataStoreUtils.readBooleanFlow
import com.mm.hamcompose.data.store.DataStoreUtils.readFloatData
import com.mm.hamcompose.data.store.DataStoreUtils.readFloatFlow
import com.mm.hamcompose.data.store.DataStoreUtils.readIntData
import com.mm.hamcompose.data.store.DataStoreUtils.readIntFlow
import com.mm.hamcompose.data.store.DataStoreUtils.readLongData
import com.mm.hamcompose.data.store.DataStoreUtils.readLongFlow
import com.mm.hamcompose.data.store.DataStoreUtils.readStringData
import com.mm.hamcompose.data.store.DataStoreUtils.readStringFlow
import com.mm.hamcompose.data.store.DataStoreUtils.saveBooleanData
import com.mm.hamcompose.data.store.DataStoreUtils.saveFloatData
import com.mm.hamcompose.data.store.DataStoreUtils.saveIntData
import com.mm.hamcompose.data.store.DataStoreUtils.saveLongData
import com.mm.hamcompose.data.store.DataStoreUtils.saveStringData
import com.mm.hamcompose.data.store.DataStoreUtils.saveSyncBooleanData
import com.mm.hamcompose.data.store.DataStoreUtils.saveSyncFloatData
import com.mm.hamcompose.data.store.DataStoreUtils.saveSyncIntData
import com.mm.hamcompose.data.store.DataStoreUtils.saveSyncLongData
import com.mm.hamcompose.data.store.DataStoreUtils.saveSyncStringData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import java.io.IOException
/**
* 版权:Zhujiang 个人版权
*
* @author zhujiang
* 创建日期:12/3/20
*
* 异步获取数据
* [getData] [readBooleanFlow] [readFloatFlow] [readIntFlow] [readLongFlow] [readStringFlow]
* 同步获取数据
* [getSyncData] [readBooleanData] [readFloatData] [readIntData] [readLongData] [readStringData]
*
* 异步写入数据
* [putData] [saveBooleanData] [saveFloatData] [saveIntData] [saveLongData] [saveStringData]
* 同步写入数据
* [putSyncData] [saveSyncBooleanData] [saveSyncFloatData] [saveSyncIntData] [saveSyncLongData] [saveSyncStringData]
*
* 异步清除数据
* [clear]
* 同步清除数据
* [clearSync]
*
* 描述:DataStore 工具类
*
*/
object DataStoreUtils {
private const val preferenceName = "WanAndroidDataStore"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(preferenceName)
private lateinit var dataStore: DataStore<Preferences>
/**
* init Context
* @param context Context
*/
fun init(context: Context) {
dataStore = context.dataStore
}
@Suppress("UNCHECKED_CAST")
fun <U> getSyncData(key: String, default: U): U {
val res = when (default) {
is Long -> readLongData(key, default)
is String -> readStringData(key, default)
is Int -> readIntData(key, default)
is Boolean -> readBooleanData(key, default)
is Float -> readFloatData(key, default)
else -> throw IllegalArgumentException("This type can be saved into DataStore")
}
return res as U
}
@Suppress("UNCHECKED_CAST")
fun <U> getData(key: String, default: U): Flow<U> {
val data = when (default) {
is Long -> readLongFlow(key, default)
is String -> readStringFlow(key, default)
is Int -> readIntFlow(key, default)
is Boolean -> readBooleanFlow(key, default)
is Float -> readFloatFlow(key, default)
else -> throw IllegalArgumentException("This type can be saved into DataStore")
}
return data as Flow<U>
}
suspend fun <U> putData(key: String, value: U) {
when (value) {
is Long -> saveLongData(key, value)
is String -> saveStringData(key, value)
is Int -> saveIntData(key, value)
is Boolean -> saveBooleanData(key, value)
is Float -> saveFloatData(key, value)
else -> throw IllegalArgumentException("This type can be saved into DataStore")
}
}
fun <U> putSyncData(key: String, value: U) {
when (value) {
is Long -> saveSyncLongData(key, value)
is String -> saveSyncStringData(key, value)
is Int -> saveSyncIntData(key, value)
is Boolean -> saveSyncBooleanData(key, value)
is Float -> saveSyncFloatData(key, value)
else -> throw IllegalArgumentException("This type can be saved into DataStore")
}
}
fun readBooleanFlow(key: String, default: Boolean = false): Flow<Boolean> =
dataStore.data
.catch {
//当读取数据遇到错误时,如果是 `IOException` 异常,发送一个 emptyPreferences 来重新使用
//但是如果是其他的异常,最好将它抛出去,不要隐藏问题
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}.map {
it[booleanPreferencesKey(key)] ?: default
}
fun readBooleanData(key: String, default: Boolean = false): Boolean {
var value = false
runBlocking {
dataStore.data.first {
value = it[booleanPreferencesKey(key)] ?: default
true
}
}
return value
}
fun readIntFlow(key: String, default: Int = 0): Flow<Int> =
dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}.map {
it[intPreferencesKey(key)] ?: default
}
fun readIntData(key: String, default: Int = 0): Int {
var value = 0
runBlocking {
dataStore.data.first {
value = it[intPreferencesKey(key)] ?: default
true
}
}
return value
}
fun readStringFlow(key: String, default: String = ""): Flow<String> =
dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}.map {
it[stringPreferencesKey(key)] ?: default
}
fun readStringData(key: String, default: String = ""): String {
var value = ""
runBlocking {
dataStore.data.first {
value = it[stringPreferencesKey(key)] ?: default
true
}
}
return value
}
fun readFloatFlow(key: String, default: Float = 0f): Flow<Float> =
dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}.map {
it[floatPreferencesKey(key)] ?: default
}
fun readFloatData(key: String, default: Float = 0f): Float {
var value = 0f
runBlocking {
dataStore.data.first {
value = it[floatPreferencesKey(key)] ?: default
true
}
}
return value
}
fun readLongFlow(key: String, default: Long = 0L): Flow<Long> =
dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}.map {
it[longPreferencesKey(key)] ?: default
}
fun readLongData(key: String, default: Long = 0L): Long {
var value = 0L
runBlocking {
dataStore.data.first {
value = it[longPreferencesKey(key)] ?: default
true
}
}
return value
}
suspend fun saveBooleanData(key: String, value: Boolean) {
dataStore.edit { mutablePreferences ->
mutablePreferences[booleanPreferencesKey(key)] = value
}
}
fun saveSyncBooleanData(key: String, value: Boolean) =
runBlocking { saveBooleanData(key, value) }
suspend fun saveIntData(key: String, value: Int) {
dataStore.edit { mutablePreferences ->
mutablePreferences[intPreferencesKey(key)] = value
}
}
fun saveSyncIntData(key: String, value: Int) = runBlocking { saveIntData(key, value) }
suspend fun saveStringData(key: String, value: String) {
dataStore.edit { mutablePreferences ->
mutablePreferences[stringPreferencesKey(key)] = value
}
}
fun saveSyncStringData(key: String, value: String) = runBlocking { saveStringData(key, value) }
suspend fun saveFloatData(key: String, value: Float) {
dataStore.edit { mutablePreferences ->
mutablePreferences[floatPreferencesKey(key)] = value
}
}
fun saveSyncFloatData(key: String, value: Float) = runBlocking { saveFloatData(key, value) }
suspend fun saveLongData(key: String, value: Long) {
dataStore.edit { mutablePreferences ->
mutablePreferences[longPreferencesKey(key)] = value
}
}
fun saveSyncLongData(key: String, value: Long) = runBlocking { saveLongData(key, value) }
suspend fun clear() {
dataStore.edit {
it.clear()
}
}
fun clearSync() {
runBlocking {
dataStore.edit {
it.clear()
}
}
}
}
@@ -0,0 +1,40 @@
package com.mm.hamcompose.di.module
import androidx.room.Room
import com.mm.hamcompose.HamApp
import com.mm.hamcompose.data.db.DbConst
import com.mm.hamcompose.data.db.history.HistoryDatabase
import com.mm.hamcompose.data.db.hotkey.HotkeyDatabase
import com.mm.hamcompose.data.db.user.UserInfoDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class DatabaseModule {
@Singleton
@Provides
fun provideHotkeyDataBase(): HotkeyDatabase {
return Room.databaseBuilder(HamApp.CONTEXT, HotkeyDatabase::class.java, DbConst.hotKeyDbName)
.build()
}
@Singleton
@Provides
fun provideUserInfoDataBase(): UserInfoDatabase {
return Room.databaseBuilder(HamApp.CONTEXT, UserInfoDatabase::class.java, DbConst.userDbName)
.build()
}
@Singleton
@Provides
fun provideHistoryDataBase(): HistoryDatabase {
return Room.databaseBuilder(HamApp.CONTEXT, HistoryDatabase::class.java, DbConst.historyDbName)
.build()
}
}
@@ -0,0 +1,46 @@
package com.mm.hamcompose.di.module
import com.mm.hamcompose.data.http.ApiCall
import com.mm.hamcompose.data.http.HttpService
import com.mm.hamcompose.data.http.interceptor.LogInterceptor
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.repository.HttpRepositoryImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import javax.inject.Singleton
//这里使用了SingletonComponent,因此 NetworkModule 绑定到 Application 的生命周期
@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {
@Singleton
@Provides
fun provideApiService(): HttpService = ApiCall.retrofit
@Singleton
@Provides
fun provideOkHttp(): OkHttpClient = ApiCall.okHttp
@Singleton
@Provides
fun provideLogInterceptor(): Interceptor = LogInterceptor()
@Provides
fun provideRepository(apiService: HttpService): HttpRepository {
return HttpRepositoryImpl(apiService)
}
// @Singleton
// @Provides
// fun provideRepo(apiService: HttpService): HttpRepository {
// return HttpRepository(apiService)
// }
}
@@ -0,0 +1,7 @@
package com.mm.hamcompose.di.scope
import javax.inject.Scope
@Scope
@MustBeDocumented
annotation class ActivityScope()
@@ -0,0 +1,7 @@
package com.mm.hamcompose.di.scope
import javax.inject.Scope
@Scope
@MustBeDocumented
annotation class ApplicationScope()
@@ -0,0 +1,7 @@
package com.mm.hamcompose.di.scope
import javax.inject.Scope
@Scope
@MustBeDocumented
annotation class FragmentScope()
@@ -0,0 +1,77 @@
package com.mm.hamcompose.repository
import androidx.paging.PagingData
import com.mm.hamcompose.data.bean.*
import com.mm.hamcompose.data.http.HttpResult
import kotlinx.coroutines.flow.Flow
//类型别名,用于定义较长的泛型类型
typealias BANNER = Flow<HttpResult<MutableList<BannerBean>>>
typealias ARTICLE = Flow<HttpResult<MutableList<Article>>>
typealias HOTKEY = Flow<HttpResult<MutableList<Hotkey>>>
typealias PARENT = Flow<HttpResult<MutableList<ParentBean>>>
typealias NAVIGATION = Flow<HttpResult<MutableList<NaviWrapper>>>
typealias USERINFO = Flow<HttpResult<UserInfo>>
typealias POINTS = Flow<HttpResult<PointsBean>>
typealias ANY = Flow<HttpResult<Any>>
typealias COLLECT = Flow<HttpResult<CollectBean>>
typealias SHARER = Flow<HttpResult<SharerBean<Article>>>
typealias ONE_PARENT = Flow<HttpResult<ParentBean>>
typealias BASIC_USERINFO = Flow<HttpResult<BasicUserInfo>>
typealias WELFARE = Flow<HttpResult<WelfareBean>>
typealias PagingAny = Flow<PagingData<Any>>
typealias PagingPoints = Flow<PagingData<PointsBean>>
typealias PagingCollect = Flow<PagingData<CollectBean>>
typealias PagingArticle = Flow<PagingData<Article>>
typealias PagingWelfare = Flow<PagingData<WelfareData>>
interface HttpRepository {
//普通请求
suspend fun getBanners(): BANNER
suspend fun getTopArticles(): ARTICLE
suspend fun getHotkeys(): HOTKEY
suspend fun getStructureList(): PARENT
suspend fun getNavigationList(): NAVIGATION
suspend fun getPublicInformation(): PARENT
suspend fun getProjectCategory(): PARENT
suspend fun register(userName: String, password: String, repassword: String): USERINFO
suspend fun login(userName: String, password: String): USERINFO
suspend fun logout(): ANY
suspend fun getMyPointsRanking(): POINTS
suspend fun getMessageCount(): Flow<HttpResult<Int>>
suspend fun getCollectUrls(): PARENT
suspend fun collectInnerArticle(id: Int): ANY
suspend fun uncollectInnerArticle(id: Int): ANY
suspend fun uncollectArticleById(id: Int, originId: Int): ANY
suspend fun addNewWebsiteCollect(title: String, linkUrl: String): ONE_PARENT
suspend fun addNewArticleCollect(title: String, linkUrl: String, author: String): COLLECT
suspend fun deleteWebsite(id: Int): ANY
suspend fun editCollectWebsite(id: Int, title: String, linkUrl: String): ANY
suspend fun getMyShareArticles(page: Int): SHARER
suspend fun getAuthorShareArticles(userId: Int, page: Int): SHARER
suspend fun deleteMyShareArticle(articleId: Int): ANY
suspend fun addMyShareArticle(title: String, link: String, shareUser: String): ANY
suspend fun getBasicUserInfo(): BASIC_USERINFO
//干货 gank.io的妹纸福利列表
suspend fun getWelfareData(page: Int, pageSize: Int): WELFARE
//分页请求
fun getIndexData(): PagingArticle
fun getSquareData(): PagingArticle
fun getWendaData(): PagingArticle
fun getProjects(cId: Int): PagingArticle
fun getPublicArticles(publicId: Int): PagingArticle
fun getStructureArticles(param: Any): PagingArticle
fun searchArticleWithKey(publicId: Int, key: String): PagingArticle
fun queryArticle(key: String): PagingArticle
fun getPointsRankings(): PagingPoints
fun getPointsRecords(): PagingPoints
fun getCollectionList(): PagingCollect
fun getUnreadMessages(): PagingAny
fun getReadedMessages(): PagingAny
fun getWelfareData(key: String): PagingWelfare
}
@@ -0,0 +1,181 @@
package com.mm.hamcompose.repository
import androidx.paging.Pager
import androidx.paging.PagingConfig
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.data.bean.WelfareBean
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.data.http.HttpService
import com.mm.hamcompose.data.http.paging.GirlPhotoPagingSource
import com.mm.hamcompose.data.http.paging.PagingFactory
import com.mm.hamcompose.ui.page.base.BaseRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
class HttpRepositoryImpl constructor(private val apiService: HttpService): BaseRepository(), HttpRepository {
//banner
override suspend fun getBanners(): BANNER = flowable { apiService.getBanners() }
//置顶文章
override suspend fun getTopArticles(): ARTICLE = flowable { apiService.getTopArticles() }
//热门标签
override suspend fun getHotkeys(): HOTKEY = flowable { apiService.getHotkeys() }
//体系分类列表
override suspend fun getStructureList(): PARENT = flowable { apiService.getStructureList() }
//导航分类列表
override suspend fun getNavigationList(): NAVIGATION = flowable { apiService.getNavigationList() }
//公众号作者列表
override suspend fun getPublicInformation(): PARENT = flowable { apiService.getPublicInformation() }
//项目分类
override suspend fun getProjectCategory(): PARENT = flowable { apiService.getProjectCategory() }
//注册
override suspend fun register(userName: String, password: String, repassword: String): USERINFO {
return flowable { apiService.register(userName, password, repassword) }
}
//登录
override suspend fun login(userName: String, password: String): USERINFO {
return flowable { apiService.login(userName, password) }
}
//退出登录
override suspend fun logout() = flowable { apiService.logout() }
//我的积分排行
override suspend fun getMyPointsRanking() = flowable { apiService.getMyPointsRanking() }
override suspend fun getMessageCount() = flowable { apiService.getMessageCount() }
override suspend fun getCollectUrls() = flowable { apiService.getCollectUrls() }
override suspend fun collectInnerArticle(id: Int): ANY {
return flowable { apiService.collectInnerArticle(id) }
}
override suspend fun uncollectInnerArticle(id: Int): ANY {
return flowable { apiService.uncollectInnerArticle(id) }
}
override suspend fun uncollectArticleById(id: Int, originId: Int): ANY {
return flowable { apiService.uncollectArticleById(id, originId) }
}
override suspend fun addNewWebsiteCollect(title: String, linkUrl: String): ONE_PARENT {
return flowable { apiService.addNewWebsiteCollect(title, linkUrl) }
}
override suspend fun addNewArticleCollect(title: String, linkUrl: String, author: String): COLLECT {
return flowable { apiService.addNewArticleCollect(title, linkUrl, author) }
}
override suspend fun deleteWebsite(id: Int) = flowable { apiService.deleteWebsite(id) }
override suspend fun editCollectWebsite(id: Int, title: String, linkUrl: String): ANY {
return flowable { apiService.editCollectWebsite(id, title, linkUrl) }
}
override suspend fun getMyShareArticles(page: Int): SHARER {
return flowable { apiService.getMyShareArticles(page) }
}
override suspend fun getAuthorShareArticles(userId: Int, page: Int): SHARER {
return flowable { apiService.getAuthorShareArticles(userId, page) }
}
override suspend fun deleteMyShareArticle(articleId: Int): ANY {
return flowable { apiService.deleteMyShareArticle(articleId) }
}
override suspend fun addMyShareArticle(title: String, link: String, shareUser: String): ANY {
return flowable { apiService.addMyShareArticle(title, link, shareUser) }
}
override suspend fun getBasicUserInfo(): BASIC_USERINFO {
return flowable { apiService.getBasicUserInfo() }
}
//福利
override suspend fun getWelfareData(page: Int, pageSize: Int): Flow<HttpResult<WelfareBean>> {
return flow {
val result = try {
val response = apiService.getWelfareList("Girl", "Girl", page, pageSize)
if (response.data != null) {
HttpResult.Success(response)
} else {
throw Exception("the result of remote's request is null")
}
} catch (ex: Exception) {
HttpResult.Error(ex)
}
emit(result)
}.flowOn(Dispatchers.IO)
}
/** 首页列表 */
override fun getIndexData() = pager { page-> apiService.getIndexList(page) }
/** 广场列表 */
override fun getSquareData() = pager { page-> apiService.getSquareData(page) }
/** 问答列表 */
override fun getWendaData() = pager { page-> apiService.getWendaData(page) }
/** 分类项目 (根据cid区分项目)*/
override fun getProjects(cId: Int) = pager { page ->
// -1=无分类,加载热门项目
if (cId == -1) {
apiService.getHotProjects(page)
} else {
apiService.getProjects(page, cId)
}
}
/** 公众号文章 */
override fun getPublicArticles(publicId: Int) = pager { apiService.getPublicArticles(publicId, it) }
/** 体系 */
override fun getStructureArticles(param: Any): PagingArticle {
return pager { page ->
when (param) {
is String -> apiService.getArticlesByAuthor(page, param)
else -> apiService.getStructureArticles(page, param as Int)
}
}
}
/** 搜索公众号*/
override fun searchArticleWithKey(publicId: Int, key: String): PagingArticle {
return pager{ page ->
apiService.getPublicArticlesWithKey(publicId, page, key)
}
}
/** 搜索文章*/
override fun queryArticle(key: String) = pager { page -> apiService.queryArticle(page, key) }
/** 积分排行榜 */
override fun getPointsRankings(): PagingPoints {
return pager(
initKey = 1,
baseConfig = PagingConfig(
pageSize = 30,
enablePlaceholders = true,
prefetchDistance = 10
)
) { page -> apiService.getPointsRankings(page) }
}
/** 积分记录 */
override fun getPointsRecords() = pager(initKey = 1) { page -> apiService.getPointsRecords(page) }
/** 收藏列表 */
override fun getCollectionList() = pager { page -> apiService.getCollectionList(page) }
/** 未读消息 */
override fun getUnreadMessages() = pager(initKey = 1) { page -> apiService.getUnreadMessage(page) }
/** 已读消息 */
override fun getReadedMessages() = pager(initKey = 1) { page -> apiService.getReadedMessage(page) }
/** 看妹纸*/
override fun getWelfareData(key: String): PagingWelfare {
return Pager(
config = PagingFactory().pagingConfig,
pagingSourceFactory = {
GirlPhotoPagingSource(apiService)
}
).flow
}
}
@@ -0,0 +1,64 @@
package com.mm.hamcompose.theme
import androidx.compose.ui.graphics.Color
import com.mm.hamcompose.HamApp
import com.mm.hamcompose.R
val Transparent = Color(0x00000000)
val themeColors = arrayOf(
Color(HamApp.CONTEXT.resources.getColor(R.color.primary)),
Color(HamApp.CONTEXT.resources.getColor(R.color.purple_200)),
Color(HamApp.CONTEXT.resources.getColor(R.color.purple_500)),
Color(HamApp.CONTEXT.resources.getColor(R.color.purple_700)),
Color(HamApp.CONTEXT.resources.getColor(R.color.teal_700)),
Color(HamApp.CONTEXT.resources.getColor(R.color.navajo_white)),
Color(HamApp.CONTEXT.resources.getColor(R.color.medium_blue)),
Color(HamApp.CONTEXT.resources.getColor(R.color.hot_pink)),
Color(HamApp.CONTEXT.resources.getColor(R.color.chocolate)),
Color(HamApp.CONTEXT.resources.getColor(R.color.dark_orange)),
Color(HamApp.CONTEXT.resources.getColor(R.color.orange)),
Color(HamApp.CONTEXT.resources.getColor(R.color.gold)),
Color(HamApp.CONTEXT.resources.getColor(R.color.yellow)),
Color(HamApp.CONTEXT.resources.getColor(R.color.fire_red)),
Color(HamApp.CONTEXT.resources.getColor(R.color.light_green)),
Color(HamApp.CONTEXT.resources.getColor(R.color.sprint_green)),
//Color(HamApp.CONTEXT.resources.getColor(R.color.azure)),
)
val splashText = Color(0x25000000)
val white = Color(0xFFFFFFFF)
val white1 = Color(0xFFF7F7F7)
val white2 = Color(0xFFEDEDED)
val white3 = Color(0xFFE5E5E5)
val white4 = Color(0xFFD5D5D5)
val white5 = Color(0xFFCCCCCC)
val black = Color(0xFF000000)
val black1 = Color(0xFF1E1E1E)
val black2 = Color(0xFF111111)
val black3 = Color(0xFF191919)
val black4 = Color(0xFF252525)
val black5 = Color(0xFF2C2C2C)
val black6 = Color(0xFF07130A)
val black7 = Color(0xFF292929)
val grey1 = Color(0xFF888888)
val grey2 = Color(0xFFCCC7BF)
val grey3 = Color(0xFF767676)
val grey4 = Color(0xFFB2B2B2)
val grey5 = Color(0xFF5E5E5E)
val green1 = Color(0xFFB0EB6E)
val green2 = Color(0xFF6DB476)
val green3 = Color(0xFF67BF63)
val red = Color(0xFFFF0000)
val red1 = Color(0xFFDF5554)
val red2 = Color(0xFFDD302E)
val red3 = Color(0xFFF77B7A)
val red4 = Color(0xFFD42220)
val red5 = Color(0xFFC51614)
val red6 = Color(0xFFF74D4B)
val red7 = Color(0xFFDC514E)
val red8 = Color(0xFFCBC7BF)
val yellow1 = Color(0xFFF6CA23)
val blue = Color(0xFF0000FF)
val info = Color(0xFF018786)
val warn = Color(0xFFD87831)
@@ -0,0 +1,29 @@
package com.mm.hamcompose.theme
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
val ToolBarHeight = 48.dp
val TabBarHeight = 48.dp
val SearchBarHeight = 42.dp
val BottomNavBarHeight = 56.dp
val ListTitleHeight = 30.dp
val PrimaryButtonHeight = 36.dp
val MediumButtonHeight = 28.dp
val SmallButtonHeight = 28.dp
val H1 = 48.sp //超大号标题
val H2 = 36.sp //大号标题
val H3 = 24.sp //主标题
val H4 = 20.sp //普通标题
val H5 = 16.sp //内容文本
val H6 = 14.sp //普通文字尺寸
val H7 = 12.sp //提示语尺寸
val ToolBarTitleSize = 18.sp
val cardCorner = 5.dp //卡片的圆角
val buttonCorner = 3.dp //按钮的圆角
val buttonHeight = 36.dp //按钮的高度
@@ -0,0 +1,11 @@
package com.mm.hamcompose.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val HamShapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
@@ -0,0 +1,194 @@
package com.mm.hamcompose.theme
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.TweenSpec
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import com.blankj.utilcode.util.SPUtils
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.systemuicontroller.rememberSystemUiController
const val THEME_COLOR_KEY = "theme_color"
const val THEME_STYLE_KEY = "theme_style"
//夜色主题
private val DarkColorPalette = HamColors(
themeUi = black1,
background = black2,
listItem = black3,
divider = black4,
textPrimary = white4,
textSecondary = grey1,
mainColor = white,
card = white1,
icon = white4,
info = info,
warn = warn,
success = green3,
error =red2,
primaryBtnBg = black1,
secondBtnBg = white1,
hot = red,
placeholder = grey1,
)
//白天主题
private val LightColorPalette = HamColors(
themeUi = themeColors[0],
background = white2,
listItem = white,
divider = white3,
textPrimary = black3,
textSecondary = grey1,
mainColor = white,
card = white1,
icon = white4,
info = info,
warn = warn,
success = green3,
error = red2,
primaryBtnBg = themeColors[0],
secondBtnBg = white3,
hot = red,
placeholder = white3,
)
var LocalHamColors = compositionLocalOf {
LightColorPalette
}
@Stable
object HamTheme {
val colors: HamColors
@Composable
get() = LocalHamColors.current
enum class Theme {
Light, Dark
}
}
@Stable
class HamColors(
themeUi: Color,
background: Color,
listItem: Color,
divider: Color,
textPrimary: Color,
textSecondary: Color,
mainColor: Color,
card: Color,
icon: Color,
info: Color,
warn: Color,
success: Color,
error: Color,
primaryBtnBg: Color,
secondBtnBg: Color,
hot: Color,
placeholder: Color,
) {
var themeUi: Color by mutableStateOf(themeUi)
internal set
var background: Color by mutableStateOf(background)
private set
var listItem: Color by mutableStateOf(listItem)
private set
var divider: Color by mutableStateOf(divider)
private set
var textPrimary: Color by mutableStateOf(textPrimary)
internal set
var textSecondary: Color by mutableStateOf(textSecondary)
private set
var mainColor: Color by mutableStateOf(mainColor)
internal set
var card: Color by mutableStateOf(card)
private set
var icon: Color by mutableStateOf(icon)
private set
var info: Color by mutableStateOf(info)
private set
var warn: Color by mutableStateOf(warn)
private set
var success: Color by mutableStateOf(success)
private set
var error: Color by mutableStateOf(error)
private set
var primaryBtnBg: Color by mutableStateOf(primaryBtnBg)
internal set
var secondBtnBg: Color by mutableStateOf(secondBtnBg)
private set
var hot: Color by mutableStateOf(hot)
private set
var placeholder: Color by mutableStateOf(placeholder)
private set
}
@Composable
fun HamTheme(
theme: HamTheme.Theme = HamTheme.Theme.Light,
content: @Composable () -> Unit
) {
val targetColors = when(theme) {
HamTheme.Theme.Light -> {
val index = SPUtils.getInstance().getInt(THEME_COLOR_KEY,0)
LightColorPalette.themeUi = themeColors[index]
LightColorPalette.primaryBtnBg = themeColors[index]
LightColorPalette
}
HamTheme.Theme.Dark -> DarkColorPalette
}
val themeUi = animateColorAsState(targetColors.themeUi, TweenSpec(600))
val background = animateColorAsState(targetColors.background, TweenSpec(600))
val listItem = animateColorAsState(targetColors.listItem, TweenSpec(600))
val divider = animateColorAsState(targetColors.divider, TweenSpec(600))
val textPrimary = animateColorAsState(targetColors.textPrimary, TweenSpec(600))
val textSecondary = animateColorAsState(targetColors.textSecondary, TweenSpec(600))
val mainColor = animateColorAsState(targetColors.mainColor, TweenSpec(600))
val card = animateColorAsState(targetColors.card, TweenSpec(600))
val icon = animateColorAsState(targetColors.icon, TweenSpec(600))
val info = animateColorAsState(targetColors.info, TweenSpec(600))
val warn = animateColorAsState(targetColors.warn, TweenSpec(600))
val success = animateColorAsState(targetColors.success, TweenSpec(600))
val error = animateColorAsState(targetColors.error, TweenSpec(600))
val primaryBtnBg = animateColorAsState(targetColors.primaryBtnBg, TweenSpec(600))
val secondBtnBg = animateColorAsState(targetColors.secondBtnBg, TweenSpec(600))
val hot = animateColorAsState(targetColors.hot, TweenSpec(600))
val placeholder = animateColorAsState(targetColors.placeholder, TweenSpec(600))
val hamColors = HamColors(
themeUi = themeUi.value,
background = background.value,
listItem = listItem.value,
divider = divider.value,
textPrimary = textPrimary.value,
textSecondary = textSecondary.value,
mainColor = mainColor.value,
card = card.value,
icon = icon.value,
primaryBtnBg = primaryBtnBg.value,
secondBtnBg = secondBtnBg.value,
info = info.value,
warn = warn.value,
success = success.value,
error = error.value,
hot = hot.value,
placeholder = placeholder.value
)
val systemUiCtrl = rememberSystemUiController()
//SideEffect用于屏蔽state,取消监听
//SideEffect {
systemUiCtrl.setStatusBarColor(hamColors.themeUi)
systemUiCtrl.setNavigationBarColor(hamColors.themeUi)
systemUiCtrl.setSystemBarsColor(hamColors.themeUi)
//}
ProvideWindowInsets {
CompositionLocalProvider(LocalHamColors provides hamColors, content = content)
}
}
@@ -0,0 +1,28 @@
package com.mm.hamcompose.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
/* Other default text styles to override
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
*/
)
@@ -0,0 +1,43 @@
package com.mm.hamcompose.ui
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.view.WindowCompat
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.ToastUtils
import com.mm.hamcompose.R
import dagger.hilt.android.AndroidEntryPoint
import kotlin.system.exitProcess
@AndroidEntryPoint
class HomeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
window.navigationBarColor = resources.getColor(R.color.transparent)
// window.setFlags(
// WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION,
// WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
// )
setContent { HomeEntry(onBackPressedDispatcher) }
}
private var cacheMills: Long = 0L
override fun onBackPressed() {
LogUtils.e("是否可以回退 ${onBackPressedDispatcher.hasEnabledCallbacks()}")
if (!onBackPressedDispatcher.hasEnabledCallbacks()) {
if (System.currentTimeMillis() - cacheMills > 1000L) {
cacheMills = System.currentTimeMillis()
ToastUtils.showShort("连按两次退出app")
} else {
this.finish()
exitProcess(0)
}
}
else super.onBackPressed()
}
}
@@ -0,0 +1,21 @@
package com.mm.hamcompose.ui
import androidx.activity.OnBackPressedDispatcher
import androidx.compose.runtime.*
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.ui.page.base.HamScaffold
import com.mm.hamcompose.ui.page.splash.SplashPage
@Composable
fun HomeEntry(backDispatcher: OnBackPressedDispatcher) {
//是否闪屏页
var isSplash by remember { mutableStateOf(true) }
if (isSplash) {
SplashPage { isSplash = false }
} else {
HamTheme { HamScaffold() }
}
}
@@ -0,0 +1,50 @@
package com.mm.hamcompose.ui.page.base
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import kotlinx.coroutines.flow.collectLatest
abstract class BaseCollectViewModel<T> constructor(
private val httpRepo: HttpRepository
) : CacheHistoryViewModel<T>() {
fun collectArticleById(id: Int) {
async {
httpRepo.collectInnerArticle(id).collectLatest { response ->
when (response) {
is HttpResult.Success -> { }
is HttpResult.Error -> {
//收藏接口,不走success判断分支
val nullNotice = "the result of remote's request is null"
if (response.exception.message==nullNotice) {
println("收藏成功(id=$id)")
message.value = "收藏成功"
} else {
message.value = response.exception.message ?: "未知异常"
}
}
}
}
}
}
fun uncollectArticleById(id: Int) {
async {
httpRepo.uncollectInnerArticle(id).collectLatest { response ->
when (response) {
is HttpResult.Success -> { }
is HttpResult.Error -> {
//收藏接口,不走success判断分支
val nullNotice = "the result of remote's request is null"
if (response.exception.message==nullNotice) {
println("取消收藏(id=$id)")
message.value = "取消收藏"
} else {
message.value = response.exception.message ?: "未知异常"
}
}
}
}
}
}
}
@@ -0,0 +1,61 @@
package com.mm.hamcompose.ui.page.base
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.mm.hamcompose.data.bean.BasicBean
import com.mm.hamcompose.data.bean.ListWrapper
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.data.http.paging.BasePagingSource
import com.mm.hamcompose.data.http.paging.PagingFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
open class BaseRepository {
fun <T> flowable(call: suspend ()-> BasicBean<T>): Flow<HttpResult<T>> {
return flow {
val result = try {
val response = call()
if (response.errorCode==0) {
if (response.data!=null) {
HttpResult.Success(response.data!!)
} else {
throw Exception("the result of remote's request is null")
}
} else {
throw Exception(response.errorMsg)
}
} catch (ex: Exception) {
HttpResult.Error(ex)
}
emit(result)
}.flowOn(Dispatchers.IO)
}
fun <T: Any> pager(
initKey: Int = 0,
baseConfig: PagingConfig = PagingFactory().pagingConfig,
callAction: suspend (page: Int)-> BasicBean<ListWrapper<T>>
): Flow<PagingData<T>> {
// config = 加载分页数据的配置项
// initialKey = 设置默认的初始页
// pagingSourceFactory = 加载分页的驱动器
return Pager(
config = baseConfig,
initialKey = initKey,
pagingSourceFactory = {
BasePagingSource {
try {
HttpResult.Success(callAction(it))
} catch (e: Exception) {
HttpResult.Error(e)
}
}
}).flow
}
}
@@ -0,0 +1,64 @@
package com.mm.hamcompose.ui.page.base
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
abstract class BaseViewModel<T> : ViewModel() {
//分类列表(装非分页加载的容器)
var list = mutableStateOf(mutableListOf<T>())
var currentListIndex = mutableStateOf(0)
var loading = mutableStateOf(false)
private var _isInited = mutableStateOf(false)
var message = mutableStateOf("")
private val isInited: Boolean
get() = _isInited.value
private fun requestInitialized() {
_isInited.value = true
}
fun resetListIndex() {
currentListIndex.value = 0
}
fun resetInitState() {
_isInited.value = false
}
fun async(block: suspend ()-> Unit) {
viewModelScope.launch { block() }
}
abstract fun start()
fun initThat(block: () -> Unit) {
if (!isInited) {
block.invoke()
requestInitialized()
}
}
fun savePosition(index: Int) {
currentListIndex.value = index
println("## save position = $index ##")
}
fun stopLoading() {
loading.value = false
}
fun startLoading() {
loading.value = true
}
open fun loadContent() { }
}
@@ -0,0 +1,40 @@
package com.mm.hamcompose.ui.page.base
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.bean.HistoryRecord
import com.mm.hamcompose.data.db.history.HistoryDatabase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
abstract class CacheHistoryViewModel<T>: BaseViewModel<T>() {
fun cacheHistory(db: HistoryDatabase, article: Article) {
async {
val history = toMapData(article)
withContext(Dispatchers.IO) {
db.historyDao().insertHistory(history)
println("成功储存到历史记录")
}
}
}
private fun toMapData(article: Article): HistoryRecord {
return with(article) {
HistoryRecord(
id = id,
title = title ?: "",
link = link ?: "",
niceDate = niceDate ?: "",
shareUser = shareUser ?: "",
userId = userId,
author = author ?: "",
superChapterId = superChapterId,
superChapterName = superChapterName ?: "",
chapterId = chapterId,
chapterName = chapterName ?: "",
desc = desc ?: ""
)
}
}
}
@@ -0,0 +1,246 @@
package com.mm.hamcompose.ui.page.base
import androidx.compose.foundation.background
import androidx.compose.material.Scaffold
import androidx.compose.material.SnackbarHost
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.NavType
import androidx.navigation.compose.*
import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.statusBarsPadding
import com.google.gson.Gson
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.data.bean.WebData
import com.mm.hamcompose.data.bean.WelfareData
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.ui.page.girls.info.GirlInfoPage
import com.mm.hamcompose.ui.page.girls.list.GirlPhotoPage
import com.mm.hamcompose.ui.page.main.category.CategoryPage
import com.mm.hamcompose.ui.page.main.category.pubaccount.author.PublicAccountAuthorPage
import com.mm.hamcompose.ui.page.main.category.pubaccount.search.PublicAccountSearch
import com.mm.hamcompose.ui.page.main.category.share.ShareArticlePage
import com.mm.hamcompose.ui.page.main.category.structure.list.StructureListPage
import com.mm.hamcompose.ui.page.main.collection.CollectionPage
import com.mm.hamcompose.ui.page.main.collection.edit.WebSiteEditPage
import com.mm.hamcompose.ui.page.main.home.HomePage
import com.mm.hamcompose.ui.page.main.home.search.SearchPage
import com.mm.hamcompose.ui.page.main.profile.ProfilePage
import com.mm.hamcompose.ui.page.main.profile.history.HistoryPage
import com.mm.hamcompose.ui.page.main.profile.message.MessagePage
import com.mm.hamcompose.ui.page.main.profile.points.PointsRankingPage
import com.mm.hamcompose.ui.page.main.profile.settings.SettingsPage
import com.mm.hamcompose.ui.page.main.profile.sharer.SharerPage
import com.mm.hamcompose.ui.page.main.profile.user.LoginPage
import com.mm.hamcompose.ui.page.main.profile.user.RegisterPage
import com.mm.hamcompose.ui.page.webview.WebViewPage
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.widget.BottomNavBarView
import com.mm.hamcompose.ui.widget.HamSnackBar
private inline fun <reified T> jsonToObject(json: String?): T? {
return runCatching {
Gson().fromJson(json, T::class.java)
}.onFailure {
println("json parse ERROR = ${it.message}")
}.getOrNull()
}
@Composable
fun HamScaffold() {
val navCtrl = rememberNavController()
val navBackStackEntry by navCtrl.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val scaffoldState = rememberScaffoldState()
Scaffold(
modifier = Modifier
.statusBarsPadding()
.navigationBarsPadding(),
bottomBar = {
when (currentDestination?.route) {
RouteName.HOME -> BottomNavBarView(navCtrl = navCtrl)
RouteName.CATEGORY -> BottomNavBarView(navCtrl = navCtrl)
RouteName.COLLECTION -> BottomNavBarView(navCtrl = navCtrl)
RouteName.PROFILE -> BottomNavBarView(navCtrl = navCtrl)
}
},
content = {
var homeIndex = remember { 0 }
var categoryIndex = remember { 0 }
NavHost(
modifier = Modifier.background(HamTheme.colors.background),
navController = navCtrl,
startDestination = RouteName.HOME
) {
//首页
composable(route = RouteName.HOME) {
HomePage(navCtrl, scaffoldState, homeIndex) { homeIndex = it }
}
//分类
composable(route = RouteName.CATEGORY) {
CategoryPage(navCtrl, categoryIndex) { categoryIndex = it }
}
//收藏
composable(route = RouteName.COLLECTION) {
CollectionPage(navCtrl, scaffoldState)
}
//我的
composable(route = RouteName.PROFILE) {
ProfilePage(navCtrl)
}
//文章搜索页
composable(
route = RouteName.ARTICLE_SEARCH + "/{id}",
arguments = listOf(navArgument("id") { type = NavType.IntType })
) {
SearchPage(navCtrl, scaffoldState)
}
//看妹子
composable(route = RouteName.GIRL_PHOTO) {
GirlPhotoPage(navCtrl)
}
//看妹子(大图)
composable(route = RouteName.GIRL_INFO) {
val args = RouteUtils.getArguments<Any>(navCtrl)
if (args != null && args is WelfareData) {
GirlInfoPage(
welfare = args,
navCtrl = navCtrl,
scaffoldState = scaffoldState
)
}
}
//公众号详情
composable(route = RouteName.PUB_ACCOUNT_DETAIL) {
val args = RouteUtils.getArguments<Any>(navCtrl)
if (args != null && args is ParentBean) {
PublicAccountAuthorPage(
parent = args,
navCtrl = navCtrl,
scaffoldState = scaffoldState
)
}
}
//体系
composable(route = RouteName.STRUCTURE_LIST) {
val args = RouteUtils.getArguments<Any>(navCtrl)
if (args != null && args is ParentBean) {
StructureListPage(
parent = args,
navCtrl = navCtrl,
scaffoldState = scaffoldState
)
}
}
//公众号搜索
composable(route = RouteName.PUB_ACCOUNT_SEARCH) {
val args = RouteUtils.getArguments<Any>(navCtrl)
if (args != null && args is ParentBean) {
PublicAccountSearch(
parent = args,
navCtrl = navCtrl,
scaffoldState = scaffoldState
)
}
}
//WebView
composable(route = RouteName.WEB_VIEW) {
val args = RouteUtils.getArguments<Any>(navCtrl)
if (args != null && args is WebData) {
WebViewPage(webData = args, navCtrl = navCtrl)
}
}
//登录
composable(route = RouteName.LOGIN) {
LoginPage(navCtrl, scaffoldState)
}
//注册
composable(route = RouteName.REGISTER) {
RegisterPage(navCtrl, scaffoldState)
}
//积分排行榜
composable(route = RouteName.RANKING) {
PointsRankingPage(navCtrl)
}
//消息
composable(route = RouteName.MESSAGE) {
MessagePage(navCtrl)
}
//设置
composable(route = RouteName.SETTINGS) {
SettingsPage(
navCtrl = navCtrl,
scaffoldState = scaffoldState
)
}
//添加网址
composable(route = RouteName.EDIT_WEBSITE) {
val args = RouteUtils.getArguments<Any>(navCtrl)
WebSiteEditPage(
website = if (args != null && args is ParentBean) args else null,
navCtrl = navCtrl,
scaffoldState = scaffoldState
)
}
// 作者/我的分享的文章列表
composable(
route = RouteName.SHARER + "/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.IntType })
) {
SharerPage(
userId = it.arguments?.getInt("userId"),
navCtrl = navCtrl,
scaffoldState = scaffoldState
)
}
//分享文章
composable(route = RouteName.SHARE_ARTICLE) {
ShareArticlePage(
navCtrl = navCtrl,
scaffoldState = scaffoldState
)
}
composable(route = RouteName.HISTORY) {
HistoryPage(navCtrl = navCtrl, scaffoldState = scaffoldState)
}
}
},
snackbarHost = {
SnackbarHost(
hostState = scaffoldState.snackbarHostState
) { data ->
println("actionLabel = ${data.actionLabel}")
HamSnackBar(data = data)
}
}
)
}
@@ -0,0 +1,77 @@
package com.mm.hamcompose.ui.page.girls.info
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.ScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import coil.compose.rememberImagePainter
import com.google.accompanist.pager.ExperimentalPagerApi
import com.mm.hamcompose.R
import com.mm.hamcompose.data.bean.WelfareData
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.HamToolBar
import com.mm.hamcompose.ui.widget.MainTitle
import com.mm.hamcompose.ui.widget.TextContent
@OptIn(ExperimentalPagerApi::class)
@Composable
fun GirlInfoPage(
welfare: WelfareData,
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
) {
Column {
HamToolBar(title = welfare.title!!, onBack = { navCtrl.back() })
PhotoView(welfare = welfare)
}
}
@Composable
private fun PhotoView(welfare: WelfareData) {
Box {
Image(
painter = rememberImagePainter(
data = welfare.url,
builder = {
crossfade(true)
placeholder(R.drawable.no_banner)
},
),
contentDescription = welfare.author,
contentScale = ContentScale.FillHeight,
modifier = Modifier.fillMaxSize()
)
MainTitle(
title = welfare.author!!,
modifier = Modifier
.padding(10.dp)
.align(Alignment.TopStart)
)
TextContent(
text = welfare.desc!!,
modifier = Modifier
.padding(10.dp)
.wrapContentSize()
.align(Alignment.BottomCenter)
.background(
brush = Brush.horizontalGradient(
listOf(HamTheme.colors.placeholder, HamTheme.colors.placeholder)
),
alpha = 0.3f
)
.padding(horizontal = 10.dp)
,
color = HamTheme.colors.mainColor
)
}
}
@@ -0,0 +1,139 @@
package com.mm.hamcompose.ui.page.girls.list
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.animation.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import coil.compose.rememberImagePainter
import coil.request.ImageRequest
import com.google.accompanist.glide.rememberGlidePainter
import com.google.accompanist.imageloading.ImageLoadState
import com.mm.hamcompose.R
import com.mm.hamcompose.data.bean.WelfareData
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.theme.themeColors
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.HamToolBar
import com.mm.hamcompose.ui.widget.TextContent
private const val TAG = "Ham"
/**
* 看妹纸 页面
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun GirlPhotoPage(
navCtrl: NavHostController,
viewModel: GirlPhotoViewModel = hiltViewModel()
) {
viewModel.start()
val girls by remember { viewModel.photoData }
val gridState = rememberLazyListState()
Column(
modifier = Modifier
.fillMaxSize()
.background(HamTheme.colors.background)
) {
HamToolBar(title = "福利", onBack = { navCtrl.back() })
if (girls.isNotEmpty()) {
LazyVerticalGrid(
cells = GridCells.Fixed(2),
contentPadding = PaddingValues(10.dp),
state = gridState,
) {
itemsIndexed(girls) { index, welfare ->
PhotoItem(
welfare = welfare,
onClick = {
RouteUtils.navTo(navCtrl, RouteName.GIRL_INFO, girls[index])
})
}
Log.d(TAG, "GirlPhotoPage: ${gridState.firstVisibleItemIndex}")
if (gridState.firstVisibleItemIndex % viewModel.pageSize > 15) {
viewModel.loadMore()
}
}
}
}
}
@SuppressLint("UseCompatLoadingForDrawables")
@Composable
fun PhotoItem(welfare: WelfareData, onClick: () -> Unit) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(240.dp)
) {
Image(
painter = rememberImagePainter(
data = welfare.url,
builder = {
crossfade(true)
placeholder(R.drawable.no_banner)
},
),
contentDescription = "空图片",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.clickable { onClick.invoke() }
)
}
}
@Preview
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimateTest() {
/** test code start */
var visible by remember { mutableStateOf(false) }
Column(modifier = Modifier.fillMaxSize()) {
TextContent(
text = "点击CrossFade",
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = 20.dp)
.clickable { visible = !visible }
)
AnimatedVisibility(
visible = visible,
enter = slideInVertically(),
exit = slideOutVertically()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(top = 20.dp)
.background(color = themeColors[0])
)
}
}
/** test code end */
}
@@ -0,0 +1,58 @@
package com.mm.hamcompose.ui.page.girls.list
import androidx.compose.runtime.mutableStateOf
import com.mm.hamcompose.data.bean.WelfareData
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest
import javax.inject.Inject
@HiltViewModel
class GirlPhotoViewModel @Inject constructor(
private var repo: HttpRepository
) : BaseViewModel<WelfareData>() {
val pageSize = 40
var page = mutableStateOf(1)
var hasNext = mutableStateOf(false)
val photoData = mutableStateOf(mutableListOf<WelfareData>())
override fun start() {
initThat { loadContent() }
}
fun loadMore() {
if (hasNext.value) {
page.value += 1
loadContent()
}
}
override fun loadContent() {
async {
repo.getWelfareData(page.value, pageSize).collectLatest { response ->
when (response) {
is HttpResult.Success -> {
val photos = response.result.data
if (!photos.isNullOrEmpty()) {
hasNext.value = true
if (photoData.value.isEmpty()) {
photoData.value = photos as MutableList<WelfareData>
} else {
photoData.value.addAll(photos)
}
} else {
hasNext.value = false
}
}
is HttpResult.Error -> {
println(response.exception.message)
}
}
}
}
}
}
@@ -0,0 +1,72 @@
package com.mm.hamcompose.ui.page.main.category
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import com.mm.hamcompose.theme.BottomNavBarHeight
import com.mm.hamcompose.ui.page.main.category.navigation.NaviPage
import com.mm.hamcompose.ui.page.main.category.pubaccount.category.PublicAccountPage
import com.mm.hamcompose.ui.page.main.category.structure.tree.StructurePage
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.widget.TextTabBar
import kotlinx.coroutines.launch
@OptIn(ExperimentalPagerApi::class)
@Composable
fun CategoryPage(
navCtrl: NavHostController,
categoryIndex: Int = 0,
viewModel: CategoryViewModel = hiltViewModel(),
onPageSelected: (position: Int) -> Unit,
) {
val titles by remember { viewModel.titles }
Box(modifier = Modifier.padding(bottom = BottomNavBarHeight)) {
Column {
val pagerState = rememberPagerState(
pageCount = titles.size,
initialPage = categoryIndex,
initialOffscreenLimit = titles.size
)
val scopeState = rememberCoroutineScope()
Row {
TextTabBar(
index = pagerState.currentPage,
tabTexts = titles,
modifier = Modifier.weight(1f),
onTabSelected = { index ->
scopeState.launch {
pagerState.scrollToPage(index)
}
},
withAdd = true,
onAddClick = {
RouteUtils.navTo(navCtrl, RouteName.SHARE_ARTICLE)
}
)
}
HorizontalPager(state = pagerState) { page ->
onPageSelected(pagerState.currentPage)
when (page) {
0 -> StructurePage(navCtrl)
1 -> NaviPage(navCtrl)
2 -> PublicAccountPage(navCtrl)
}
}
}
}
}
@@ -0,0 +1,32 @@
package com.mm.hamcompose.ui.page.main.category
import androidx.compose.runtime.mutableStateOf
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.data.bean.TabTitle
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class CategoryViewModel @Inject constructor() : BaseViewModel<ParentBean>() {
val titles = mutableStateOf(
mutableListOf(
TabTitle(201, "体系"),
TabTitle(202, "导航"),
TabTitle(203, "公众号"),
)
)
override fun start() {
}
override fun onCleared() {
super.onCleared()
println("CategoryViewModel ==> onClear")
}
}
@@ -0,0 +1,119 @@
package com.mm.hamcompose.ui.page.main.category.navigation
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.google.accompanist.flowlayout.FlowRow
import com.mm.hamcompose.data.bean.NaviWrapper
import com.mm.hamcompose.data.bean.WebData
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.widget.LabelTextButton
import com.mm.hamcompose.ui.widget.ListTitle
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NaviPage(
navCtrl: NavHostController,
viewModel: NaviViewModel = hiltViewModel()
) {
viewModel.start()
val naviData by remember { viewModel.list }
val isLoading by remember { viewModel.loading }
val currentPosition by remember { viewModel.currentListIndex }
val listState = rememberLazyListState(currentPosition)
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
contentPadding = PaddingValues(vertical = 10.dp)
) {
if (isLoading) {
items(6) {
NaviItem(
wrapper = NaviWrapper(null, -1, ""),
isLoading = isLoading,
)
}
} else {
naviData.forEachIndexed { index, naviBean ->
stickyHeader { ListTitle(title = naviBean.name ?: "标题") }
item {
NaviItem(naviBean, onSelected = {
viewModel.savePosition(listState.firstVisibleItemIndex)
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, it)
})
if (index <= naviData.size - 1) {
Divider(
startIndent = 10.dp,
color = HamTheme.colors.divider,
thickness = 0.8f.dp
)
}
Spacer(modifier = Modifier.height(10.dp))
}
}
}
}
}
@Composable
fun NaviItem(
wrapper: NaviWrapper,
isLoading: Boolean = false,
onSelected: (WebData) -> Unit = {}
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp)
) {
if (isLoading) {
ListTitle(title = "我是标题")
FlowRow(
modifier = Modifier.padding(top = 10.dp)
) {
for (i in 0..7) {
LabelTextButton(
text = "android",
modifier = Modifier.padding(start = 5.dp, bottom = 5.dp),
isLoading = true
)
}
}
Spacer(modifier = Modifier.height(10.dp))
} else {
if (!wrapper.articles.isNullOrEmpty()) {
FlowRow(
modifier = Modifier.padding(top = 10.dp)
) {
for (item in wrapper.articles!!) {
LabelTextButton(
text = item.title ?: "android",
modifier = Modifier.padding(start = 5.dp, bottom = 5.dp),
onClick = {
val webData = WebData(item.title, item.link!!)
onSelected(webData)
}
)
}
}
Spacer(modifier = Modifier.height(10.dp))
}
}
}
}
@@ -0,0 +1,36 @@
package com.mm.hamcompose.ui.page.main.category.navigation
import com.mm.hamcompose.data.bean.NaviWrapper
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest
import javax.inject.Inject
@HiltViewModel
class NaviViewModel @Inject constructor(
private val httpRepo: HttpRepository
): BaseViewModel<NaviWrapper>() {
override fun loadContent() {
async {
httpRepo.getNavigationList().collectLatest { response ->
when (response) {
is HttpResult.Success -> {
list.value = response.result
}
is HttpResult.Error -> {
}
}
}
}
}
override fun start() {
initThat { loadContent() }
}
}
@@ -0,0 +1,125 @@
package com.mm.hamcompose.ui.page.main.category.pubaccount.author
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ScaffoldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import androidx.paging.compose.itemsIndexed
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.data.bean.WebData
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.theme.ToolBarHeight
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.*
@Composable
fun PublicAccountAuthorPage(
parent: ParentBean?,
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: PublicAccountAuthorViewModel = hiltViewModel()
) {
parent ?: return
viewModel.setPublicId(parent.id)
viewModel.start()
val articles = viewModel.publicData.value?.collectAsLazyPagingItems()
val isLoaded = articles?.loadState?.prepend?.endOfPaginationReached ?: false
val refreshing by remember { viewModel.isRefreshing }
val message by remember { viewModel.message }
val swipeRefreshState = rememberSwipeRefreshState(refreshing)
val currentPosition by remember { viewModel.currentListIndex }
val listState = rememberLazyListState(currentPosition)
val coroutineScope = rememberCoroutineScope()
if (message.isNotEmpty()) {
popupSnackBar(coroutineScope, scaffoldState, SNACK_INFO, message)
viewModel.message.value = ""
}
Column(
modifier = Modifier.background(HamTheme.colors.background)
) {
HamToolBar(
title = parent.name ?: "",
onBack = {
navCtrl.back()
},
onRightClick = {
RouteUtils.navTo(navCtrl, RouteName.PUB_ACCOUNT_SEARCH, parent)
},
imageVector = Icons.Default.Search
)
SwipeRefresh(
state = swipeRefreshState,
modifier = Modifier.align(Alignment.CenterHorizontally),
onRefresh = {
viewModel.savePosition(0)
viewModel.clearCache()
viewModel.initPublicArticles()
},
) {
LazyColumn(state = listState) {
if (isLoaded) {
if (articles!!.itemCount > 0) {
items(articles) { item ->
SimpleListItemView(
data = item!!,
onClick = {
viewModel.saveDataToHistory(item)
viewModel.savePosition(listState.firstVisibleItemIndex)
RouteUtils.navTo(
navCtrl,
RouteName.WEB_VIEW,
WebData(item.title, item.link!!)
)
},
onCollectClick = {
if (item.collect) {
viewModel.uncollectArticleById(it)
item.collect = false
} else {
viewModel.collectArticleById(it)
item.collect = true
}
}
)
}
} else {
item { EmptyView() }
}
} else {
items(6) {
SimpleListItemView(
data = Article(),
isLoading = true
)
}
}
}
}
}
}
@@ -0,0 +1,61 @@
package com.mm.hamcompose.ui.page.main.category.pubaccount.author
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.blankj.utilcode.util.LogUtils
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.db.history.HistoryDatabase
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.repository.PagingArticle
import com.mm.hamcompose.ui.page.base.BaseCollectViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class PublicAccountAuthorViewModel @Inject constructor(
private var repo: HttpRepository,
private val db: HistoryDatabase,
): BaseCollectViewModel<Article>(repo) {
/**
* 某个技术公众号的列表
*/
var publicData = MutableLiveData<PagingArticle?>(null)
var isRefreshing = mutableStateOf(false)
private var authorId = mutableStateOf(-1)
override fun start() {
initThat { initPublicArticles() }
}
fun setPublicId(id: Int) {
authorId.value = id
}
fun clearCache() {
isRefreshing.value = true
publicData.value = null
}
fun initPublicArticles() {
if (publicData.value==null) {
publicData.value = getPublicArticles()
isRefreshing.value = publicData.value==null
}
}
private fun getPublicArticles() = repo.getPublicArticles(authorId.value).cachedIn(viewModelScope)
override fun onCleared() {
LogUtils.e("ViewModel执行onCleared()")
super.onCleared()
}
fun saveDataToHistory(article: Article) {
cacheHistory(db, article)
}
}
@@ -0,0 +1,147 @@
package com.mm.hamcompose.ui.page.main.category.pubaccount.category
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.theme.white1
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PublicAccountPage(
navCtrl: NavHostController,
viewModel: PublicAccountViewModel = hiltViewModel(),
) {
viewModel.start()
val publicNoData by remember { viewModel.list }
val currentPosition by remember { viewModel.currentListIndex }
val listState = rememberLazyListState(currentPosition)
LazyVerticalGrid(
cells = GridCells.Fixed(2),
modifier = Modifier
.background(HamTheme.colors.background)
.wrapContentHeight()
.padding(10.dp),
state = listState
) {
itemsIndexed(publicNoData) { index, item ->
Box(Modifier.padding(vertical = 5.dp)) {
when (index % 4) {
0 -> PublicAccountItem(
parent = item,
click = {
viewModel.savePosition(listState.firstVisibleItemIndex)
navToPublicAccountDetail(navCtrl, it)
},
isPrimary = true,
roundedCorner = RoundedCornerShape(topStart = 5.dp, bottomStart = 5.dp)
)
1 -> PublicAccountItem(
parent = item,
click = {
viewModel.savePosition(listState.firstVisibleItemIndex)
navToPublicAccountDetail(navCtrl, it)
},
isPrimary = false,
roundedCorner = RoundedCornerShape(topEnd = 5.dp, bottomEnd = 5.dp)
)
2 -> PublicAccountItem(
parent = item,
click = {
viewModel.savePosition(listState.firstVisibleItemIndex)
navToPublicAccountDetail(navCtrl, it)
},
isPrimary = false,
roundedCorner = RoundedCornerShape(topStart = 5.dp, bottomStart = 5.dp)
)
3 -> PublicAccountItem(
parent = item,
click = {
viewModel.savePosition(listState.firstVisibleItemIndex)
navToPublicAccountDetail(navCtrl, it)
},
isPrimary = true,
roundedCorner = RoundedCornerShape(topEnd = 5.dp, bottomEnd = 5.dp)
)
}
}
}
}
}
private fun navToPublicAccountDetail(navCtrl: NavHostController, parent: ParentBean) {
RouteUtils.navTo(navCtrl, RouteName.PUB_ACCOUNT_DETAIL, parent)
}
@Composable
fun SpecialText(
parent: ParentBean,
textColor: Color,
bgColor: Color,
shape: RoundedCornerShape,
onClick: (ParentBean) -> Unit
) {
Text(
text = parent.name!!,
textAlign = TextAlign.Center,
fontSize = 18.sp,
color = textColor,
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.background(
color = bgColor,
shape = shape
)
.padding(top = 30.dp)
.clickable {
onClick(parent)
},
fontWeight = FontWeight.W500,
)
}
@Composable
private fun PublicAccountItem(
parent: ParentBean,
click: (ParentBean) -> Unit,
isPrimary: Boolean = true,
roundedCorner: RoundedCornerShape = RoundedCornerShape(5.dp)
) {
SpecialText(
parent = parent,
textColor = if (isPrimary) white1 else HamTheme.colors.textSecondary,
bgColor = if (isPrimary) HamTheme.colors.themeUi else white1,
shape = roundedCorner
) {
click(it)
}
}
@@ -0,0 +1,40 @@
package com.mm.hamcompose.ui.page.main.category.pubaccount.category
import androidx.compose.runtime.mutableStateOf
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest
import javax.inject.Inject
@HiltViewModel
class PublicAccountViewModel @Inject constructor(
private var httpRepo: HttpRepository
): BaseViewModel<ParentBean>() {
//公众号ID
private var publicId = mutableStateOf(-1)
override fun start() {
initThat { loadContent() }
}
override fun loadContent() {
async {
httpRepo.getPublicInformation().collectLatest { response ->
when (response) {
is HttpResult.Success -> {
list.value = response.result
}
is HttpResult.Error -> {
}
}
}
}
}
}
@@ -0,0 +1,193 @@
package com.mm.hamcompose.ui.page.main.category.pubaccount.search
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Icon
import androidx.compose.material.ScaffoldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import androidx.paging.compose.itemsIndexed
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.data.bean.WebData
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.theme.ToolBarHeight
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.EmptyView
import com.mm.hamcompose.ui.widget.SNACK_INFO
import com.mm.hamcompose.ui.widget.SimpleListItemView
import com.mm.hamcompose.ui.widget.popupSnackBar
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun PublicAccountSearch(
parent: ParentBean?,
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: PublicAccountSearchViewModel = hiltViewModel(),
) {
parent ?: return
viewModel.setPublicId(parent.id)
viewModel.start()
val searchResult = viewModel.searchResult.value?.collectAsLazyPagingItems()
val searchContent by remember { viewModel.searchContent }
val refreshing by remember { viewModel.isRefreshing }
val message by remember { viewModel.message }
val swipeRefreshState = rememberSwipeRefreshState(refreshing)
val keyboardCtrl = LocalSoftwareKeyboardController.current
val currentPosition by remember { viewModel.currentListIndex }
val listState = rememberLazyListState(currentPosition)
val coroutineScope = rememberCoroutineScope()
if (message.isNotEmpty()) {
popupSnackBar(coroutineScope, scaffoldState, SNACK_INFO, message)
viewModel.message.value = ""
}
Column {
SearchHead(
key = searchContent,
viewModel = viewModel,
onKeyChange = {
viewModel.searchContent.value = it
},
backPress = {
navCtrl.back()
},
softwareKeyboardController = keyboardCtrl
)
if (searchContent.isNotEmpty()) {
SwipeRefresh(
state = swipeRefreshState,
onRefresh = {
viewModel.refreshSearch(searchContent)
},
modifier = Modifier.fillMaxSize()
) {
if (searchResult != null) {
LazyColumn(state = listState) {
itemsIndexed(searchResult) { index, item ->
SimpleListItemView(
data = item!!,
onClick = {
viewModel.saveDataToHistory(item)
viewModel.savePosition(listState.firstVisibleItemIndex)
RouteUtils.navTo(
navCtrl, RouteName.WEB_VIEW, WebData(item.title, item.link!!))
},
onCollectClick = {
if (item.collect) {
viewModel.uncollectArticleById(it)
searchResult.peek(index)?.collect = false
} else {
viewModel.collectArticleById(it)
searchResult.peek(index)?.collect = true
}
})
}
}
}
}
} else {
EmptyView(
tips = "输入关键字搜索",
imageVector = Icons.Default.Search,
onClick = {
keyboardCtrl?.hide()
}
)
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SearchHead(
key: String,
viewModel: PublicAccountSearchViewModel,
onKeyChange: (word: String) -> Unit,
backPress: () -> Unit,
softwareKeyboardController: SoftwareKeyboardController?
) {
Box(
Modifier
.fillMaxWidth()
.height(ToolBarHeight)
.background(HamTheme.colors.themeUi)
) {
Row(Modifier.align(Alignment.Center)) {
Icon(
Icons.Default.ArrowBack,
null,
Modifier
.clickable(onClick = backPress)
.align(Alignment.CenterVertically)
.padding(horizontal = 10.dp),
tint = HamTheme.colors.mainColor
)
BasicTextField(
value = key,
onValueChange = {
onKeyChange(it)
if (it.trim().isNotEmpty()) {
viewModel.search(it)
}
},
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.height(28.dp)
.padding(end = 10.dp)
.background(
color = HamTheme.colors.mainColor,
shape = RoundedCornerShape(14.dp),
)
.padding(start = 10.dp, top = 4.dp)
.align(Alignment.CenterVertically),
maxLines = 1,
singleLine = true,
keyboardActions = KeyboardActions {
softwareKeyboardController?.hide()
},
textStyle = TextStyle(
color = HamTheme.colors.textSecondary,
fontWeight = FontWeight.Normal,
fontSize = 14.sp
),
)
}
}
}
@@ -0,0 +1,58 @@
package com.mm.hamcompose.ui.page.main.category.pubaccount.search
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.db.history.HistoryDatabase
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.repository.PagingArticle
import com.mm.hamcompose.ui.page.base.BaseCollectViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class PublicAccountSearchViewModel @Inject constructor(
private val repo: HttpRepository,
private val db: HistoryDatabase,
): BaseCollectViewModel<Article>(repo) {
/**
* 在某个公众号下搜索关键字
*/
//var searchText = MutableLiveData("")
var isRefreshing = mutableStateOf(false)
var searchContent = mutableStateOf("")
val searchResult = MutableLiveData<PagingArticle?>(null)
private var publicId = mutableStateOf(-1)
override fun start() {
}
fun setPublicId(id: Int) {
this.publicId.value = id
}
private fun searchArticleWithKey(key: String) =
repo.searchArticleWithKey(publicId.value, key).cachedIn(viewModelScope)
fun refreshSearch(key: String) {
resetListIndex()
search(key)
}
fun search(key: String) {
searchResult.value = null
searchResult.value = searchArticleWithKey(key)
isRefreshing.value = searchResult.value==null
}
fun saveDataToHistory(article: Article) {
cacheHistory(db, article)
}
}
@@ -0,0 +1,109 @@
package com.mm.hamcompose.ui.page.main.category.share
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.ScaffoldState
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.mm.hamcompose.R
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.*
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ShareArticlePage(
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: ShareArticleViewModel = hiltViewModel()
) {
val title by remember { viewModel.title }
val shareUser by remember { viewModel.shareUser }
val linkUrl by remember { viewModel.linkUrl }
var errorMsg by remember { viewModel.errorMessage }
var snackLabel by remember { mutableStateOf(SNACK_WARN) }
val keyboardController = LocalSoftwareKeyboardController.current
if (errorMsg.isNotEmpty()) {
popupSnackBar(
scope = rememberCoroutineScope(),
scaffoldState = scaffoldState,
label = if (errorMsg == "分享成功") SNACK_SUCCESS else snackLabel,
message = errorMsg
)
errorMsg = ""
}
Column {
HamToolBar(
title = "分享文章",
onBack = { navCtrl.back() },
rightText = "保存",
onRightClick = {
keyboardController?.hide()
if (title.isNullOrEmpty()) {
snackLabel = SNACK_WARN
errorMsg = "标题不能为空"
return@HamToolBar
}
if (linkUrl.isNullOrEmpty()) {
snackLabel = SNACK_WARN
errorMsg = "链接不能为空"
return@HamToolBar
}
viewModel.addShareArticle()
}
)
LazyColumn(
modifier = Modifier.weight(1f)
) {
item {
LabelEditView(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 20.dp),
text = title ?: "",
labelText = stringResource(id = R.string.title),
hintText = "请输入标题(限100字以内)",
onValueChanged = {
viewModel.title.value = it
},
onDeleteClick = { viewModel.title.value = "" },
)
}
item {
LabelEditView(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 20.dp),
text = shareUser ?: "",
labelText = stringResource(id = R.string.author),
hintText = "默认使用昵称,没有昵称则使用用户名",
onValueChanged = {
viewModel.shareUser.value = it
},
onDeleteClick = { viewModel.shareUser.value = "" },
)
}
item {
LabelEditView(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 20.dp),
text = linkUrl ?: "",
labelText = stringResource(id = R.string.link),
hintText = stringResource(id = R.string.hint_text_website_url),
onValueChanged = {
viewModel.linkUrl.value = it
},
onDeleteClick = { viewModel.linkUrl.value = "" },
)
}
}
}
}
@@ -0,0 +1,54 @@
package com.mm.hamcompose.ui.page.main.category.share
import androidx.compose.runtime.mutableStateOf
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.db.user.UserInfoDatabase
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest
import javax.inject.Inject
@HiltViewModel
class ShareArticleViewModel @Inject constructor(
private val repo: HttpRepository,
private val db: UserInfoDatabase
): BaseViewModel<Article>() {
var title = mutableStateOf<String?>(null)
var shareUser = mutableStateOf<String?>(null)
var linkUrl = mutableStateOf<String?>(null)
var errorMessage = mutableStateOf("")
override fun start() {
}
fun addShareArticle() {
async {
if (shareUser.value.isNullOrEmpty()) {
val users = db.userInfoDao().queryUserInfo()
if (!users.isNullOrEmpty()) {
with(users[0]!!) {
shareUser.value = if (nickname.isEmpty()) username else nickname
}
}
}
repo.addMyShareArticle(title.value!!, linkUrl.value!!, shareUser.value!!)
.collectLatest { response ->
when(response) {
is HttpResult.Success -> { }
is HttpResult.Error -> {
val isShare = response.exception.message == "the result of remote's request is null"
if (isShare) {
errorMessage.value = "分享成功"
}
}
}
}
}
}
}
@@ -0,0 +1,220 @@
package com.mm.hamcompose.ui.page.main.category.structure.list
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Icon
import androidx.compose.material.ScaffoldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalTextInputService
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemsIndexed
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.theme.ToolBarHeight
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.MediumTitle
import com.mm.hamcompose.ui.widget.MultiStateItemView
import com.mm.hamcompose.ui.widget.SNACK_INFO
import com.mm.hamcompose.ui.widget.popupSnackBar
internal const val TIPS_TEXT = "输入作者名称"
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun StructureListPage(
parent: ParentBean?,
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: StructureListViewModel = hiltViewModel()
) {
parent ?: return
viewModel.setId(parent.id)
viewModel.start()
var isShowInput by remember { mutableStateOf(false) }
val authorName by remember { viewModel.authorName }
val message by remember { viewModel.message }
val articles = viewModel.articles.value?.collectAsLazyPagingItems()
val refreshing by remember { viewModel.isRefreshing }
val swipeRefreshState = rememberSwipeRefreshState(refreshing)
val currentPosition by remember { viewModel.currentListIndex }
val listState = rememberLazyListState(currentPosition)
val keyboard = LocalSoftwareKeyboardController.current
val coroutineScope = rememberCoroutineScope()
if (message.isNotEmpty()) {
popupSnackBar(coroutineScope, scaffoldState, SNACK_INFO, message)
viewModel.message.value = ""
}
Column {
if (isShowInput) {
InputSearchBar(
keyWord = authorName,
onTextChange = {
viewModel.authorName.value = it
if (it.isEmpty()) {
viewModel.refresh("")
}
},
onSearchClick = {
viewModel.searchByAuthor(it)
},
navCtrl = navCtrl
)
}
SwipeRefresh(
state = swipeRefreshState,
onRefresh = { viewModel.refresh(authorName) },
modifier = Modifier
.align(Alignment.CenterHorizontally)
.pointerInput(Unit) {
detectTapGestures(
onPress = {
keyboard?.hide()
}
)
}
) {
LazyColumn(
state = listState,
) {
if (articles != null && articles.itemCount > 0) {
itemsIndexed(articles) { index, item ->
MultiStateItemView(
data = item!!,
onSelected = { data ->
viewModel.saveDataToHistory(item)
viewModel.savePosition(listState.firstVisibleItemIndex)
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, data)
},
onCollectClick = {
if (item.collect) {
viewModel.uncollectArticleById(it)
articles.peek(index)?.collect = false
} else {
viewModel.collectArticleById(it)
articles.peek(index)?.collect = true
}
},
onUserClick = { userId ->
RouteUtils.navTo(navCtrl, RouteName.SHARER, userId)
})
}
} else {
items(5) {
MultiStateItemView(data = Article(), isLoading = true)
}
}
isShowInput = listState.firstVisibleItemIndex <= 0
}
}
}
}
/**
* 搜索框
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun InputSearchBar(
keyWord: String,
onTextChange: (text: String) -> Unit,
onSearchClick: (key: String) -> Unit,
navCtrl: NavHostController
) {
val inputService = LocalTextInputService.current
Box(
modifier = Modifier
.fillMaxWidth()
.height(ToolBarHeight)
.background(HamTheme.colors.themeUi)
) {
Row(
modifier = Modifier
.align(Alignment.Center)
.padding(10.dp)
) {
Icon(
Icons.Default.ArrowBack,
null,
Modifier
.clickable(onClick = { navCtrl.back() })
.align(Alignment.CenterVertically)
.padding(end = 10.dp)
.size(25.dp),
tint = HamTheme.colors.mainColor
)
BasicTextField(
value = keyWord,
onValueChange = {
onTextChange.invoke(it)
},
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.height(28.dp)
.background(
color = HamTheme.colors.mainColor,
shape = RoundedCornerShape(14.dp),
)
.padding(start = 10.dp, top = 4.dp)
.align(Alignment.CenterVertically),
maxLines = 1,
singleLine = true,
textStyle = TextStyle(
color = HamTheme.colors.textSecondary,
fontWeight = FontWeight.Normal,
fontSize = 14.sp
),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions { inputService?.hideSoftwareKeyboard() },
cursorBrush = SolidColor(HamTheme.colors.textSecondary)
)
if (keyWord.trim().isNotEmpty()) {
MediumTitle(
title = "搜索",
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 10.dp)
.combinedClickable(onClick = { onSearchClick(keyWord) }),
color = HamTheme.colors.mainColor,
)
}
}
}
}
@@ -0,0 +1,67 @@
package com.mm.hamcompose.ui.page.main.category.structure.list
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.db.history.HistoryDatabase
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.repository.PagingArticle
import com.mm.hamcompose.ui.page.base.BaseCollectViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class StructureListViewModel @Inject constructor(
private val repo: HttpRepository,
private val db: HistoryDatabase,
): BaseCollectViewModel<Article>(repo) {
private var cid = -1
//某个文章的列表
var articles = MutableLiveData<PagingArticle?>(null)
var isRefreshing = mutableStateOf(true)
var authorName = mutableStateOf("")
fun setId(id: Int) {
cid = id
}
override fun start() {
initThat { initArticles() }
}
fun refresh(author: String) {
resetListIndex()
isRefreshing.value = true
articles.value = null
if (author.isEmpty()) {
initArticles()
} else {
searchByAuthor(author)
}
}
private fun initArticles() {
if (articles.value==null) {
articles.value = getStructureArticles()
isRefreshing.value = articles.value==null
}
}
private fun getStructureArticles() = repo.getStructureArticles(cid).cachedIn(viewModelScope)
fun searchByAuthor(author: String) {
isRefreshing.value = true
articles.value = null
articles.value = repo.getStructureArticles(author).cachedIn(viewModelScope)
isRefreshing.value = articles.value==null
}
fun saveDataToHistory(article: Article) {
cacheHistory(db, article)
}
}
@@ -0,0 +1,114 @@
package com.mm.hamcompose.ui.page.main.category.structure.tree
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.google.accompanist.flowlayout.FlowRow
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.widget.LabelTextButton
import com.mm.hamcompose.ui.widget.ListTitle
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun StructurePage(
navCtrl: NavHostController,
viewModel: StructureViewModel = hiltViewModel()
) {
viewModel.start()
val systemData by remember { viewModel.list }
val isLoading by remember { viewModel.loading }
val currentPosition by remember { viewModel.currentListIndex }
val listState = rememberLazyListState(currentPosition)
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(HamTheme.colors.background),
state = listState,
contentPadding = PaddingValues(vertical = 10.dp)
) {
if (isLoading) {
items(5) {
StructureItem(ParentBean(null), isLoading = true)
}
} else {
systemData.forEachIndexed { position, chapter1 ->
stickyHeader { ListTitle(title = chapter1.name ?: "标题") }
item {
StructureItem(chapter1, onSelect = { parent->
viewModel.savePosition(listState.firstVisibleItemIndex)
RouteUtils.navTo(navCtrl, RouteName.STRUCTURE_LIST, parent)
})
if (position <= systemData.size - 1) {
Divider(startIndent = 10.dp, color = HamTheme.colors.divider, thickness = 0.8f.dp)
}
Spacer(modifier = Modifier.height(10.dp))
}
}
}
}
}
@Composable
fun StructureItem(
bean: ParentBean,
isLoading: Boolean = false,
onSelect: (parent: ParentBean) -> Unit = {},
) {
Column(
modifier = Modifier.fillMaxWidth().padding(top = 10.dp)
) {
if (isLoading) {
ListTitle(title = "我都标题", isLoading = true)
FlowRow(
modifier = Modifier.padding(horizontal = 10.dp)
) {
for (i in 0..7) {
LabelTextButton(
text = "android",
modifier = Modifier.padding(start = 5.dp, bottom = 5.dp),
isLoading = true
)
}
}
Spacer(modifier = Modifier.height(10.dp))
} else {
if (!bean.children.isNullOrEmpty()) {
FlowRow(
modifier = Modifier.padding(horizontal = 10.dp)
) {
for (item in bean.children!!) {
LabelTextButton(
text = item.name ?: "android",
modifier = Modifier.padding(start = 5.dp, bottom = 5.dp),
onClick = {
onSelect(item)
}
)
}
}
Spacer(modifier = Modifier.height(10.dp))
}
}
}
}
@@ -0,0 +1,38 @@
package com.mm.hamcompose.ui.page.main.category.structure.tree
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest
import javax.inject.Inject
private const val TAG = "StructureViewModel ==> "
@HiltViewModel
class StructureViewModel @Inject constructor(
private var repo: HttpRepository,
): BaseViewModel<ParentBean>() {
override fun loadContent() {
startLoading()
async {
repo.getStructureList().collectLatest { response ->
when (response) {
is HttpResult.Success -> {
list.value = response.result
}
is HttpResult.Error -> {
println(TAG + response.exception.message)
}
}
stopLoading()
}
}
}
override fun start() {
initThat { loadContent() }
}
}
@@ -0,0 +1,184 @@
package com.mm.hamcompose.ui.page.main.collection
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ScaffoldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.Info
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import androidx.paging.compose.itemsIndexed
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.mm.hamcompose.data.bean.CollectBean
import com.mm.hamcompose.data.bean.WebData
import com.mm.hamcompose.theme.BottomNavBarHeight
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.widget.*
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CollectionPage(
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: CollectionViewModel = hiltViewModel()
) {
viewModel.start()
var dialogAlert by remember { mutableStateOf(false) }
val login by remember { viewModel.isLogin }
val titles by remember { viewModel.titles }
val webUrls by remember { viewModel.webUrlList }
var labelIndex by remember { mutableStateOf(0) }
val collectArticles = viewModel.collectArticles.value?.collectAsLazyPagingItems()
val isLoaded = collectArticles?.loadState?.prepend?.endOfPaginationReached ?: false
val currentPosition by remember { viewModel.currentListIndex }
val listState = rememberLazyListState(currentPosition)
val message by remember { viewModel.message }
val scope = rememberCoroutineScope()
val isRefreshing by remember { viewModel.isRefreshing }
val refreshState = rememberSwipeRefreshState(isRefreshing)
if (message.isNotEmpty()) {
popupSnackBar(scope, scaffoldState, SNACK_INFO, "已删除")
viewModel.message.value = ""
}
if (dialogAlert) {
SelectAlertDialog(
title = "提示",
content = "请选择以下操作",
primaryButtonText = "删除",
secondButtonText = "编辑",
onPrimaryButtonClick = {
viewModel.deleteWebsite(webUrls!![labelIndex].id)
},
onSecondButtonClick = {
RouteUtils.navTo(navCtrl, RouteName.EDIT_WEBSITE, webUrls!![labelIndex])
},
onDismiss = { dialogAlert = false }
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(bottom = BottomNavBarHeight)
) {
HamToolBar(
title = "我的收藏",
onRightClick = {
RouteUtils.navTo(navCtrl, RouteName.EDIT_WEBSITE)
},
imageVector = if (login) Icons.Default.Edit else null
)
if (!login) {
EmptyView(
tips = "点击登录",
imageVector = Icons.Default.Face
) {
RouteUtils.navTo(navCtrl, RouteName.LOGIN)
}
} else {
SwipeRefresh(
state = refreshState,
onRefresh = {
viewModel.refresh()
}
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
contentPadding = PaddingValues(top = 10.dp)
) {
if (!webUrls.isNullOrEmpty()) {
stickyHeader {
ListTitle(title = titles[1].text)
}
item {
FlowRow(
modifier = Modifier.padding(10.dp)
) {
webUrls?.forEachIndexed { index, website ->
LabelTextButton(
text = website.name ?: "标签",
modifier = Modifier.padding(end = 10.dp, bottom = 10.dp),
onClick = {
viewModel.resetListIndex()
RouteUtils.navTo(
navCtrl,
RouteName.WEB_VIEW,
WebData(website.name, website.link!!)
)
},
onLongClick = {
labelIndex = index
dialogAlert = true
}
)
}
}
}
}
stickyHeader {
ListTitle(title = titles[0].text)
}
if (isLoaded) {
if (collectArticles!!.itemCount > 0) {
items(collectArticles) { collectItem ->
CollectListItemView(
collectItem!!,
onClick = {
viewModel.savePosition(listState.firstVisibleItemIndex)
RouteUtils.navTo(
navCtrl,
RouteName.WEB_VIEW,
WebData(collectItem.title, collectItem.link)
)
}, onDeleteClick = {
with(collectItem) {
viewModel.uncollectArticle(id, originId)
}
})
}
} else {
item {
EmptyView(tips = "啥都没有~", imageVector = Icons.Default.Create) {
RouteUtils.navTo(navCtrl, RouteName.EDIT_WEBSITE)
}
}
}
} else {
items(6) {
CollectListItemView(
collect = CollectBean(),
isLoading = true
)
}
}
}
}
}
}
}
@@ -0,0 +1,135 @@
package com.mm.hamcompose.ui.page.main.collection
import android.annotation.SuppressLint
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.data.bean.TabTitle
import com.mm.hamcompose.data.db.user.UserInfoDatabase
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.repository.PagingCollect
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@HiltViewModel
class CollectionViewModel @Inject constructor(
private val repo: HttpRepository,
private val db: UserInfoDatabase
) : BaseViewModel<ParentBean>() {
val titles = mutableStateOf(
mutableListOf(
TabTitle(301, "文章列表"),
TabTitle(302, "我的网址"),
)
)
var collectArticles = MutableLiveData<PagingCollect?>(null)
var webUrlList = mutableStateOf<MutableList<ParentBean>?>(null)
var isRefreshing = mutableStateOf(false)
var isLogin = mutableStateOf(false)
override fun start() {
checkLoginState()
}
private fun checkLoginState() {
async {
flow { emit(db.userInfoDao().queryUserInfo()) }
.flowOn(Dispatchers.IO)
.collectLatest { users ->
isLogin.value = users.isNotEmpty()
if (isLogin.value && isNotInit()) {
initData()
}
}
}
}
private fun isNotInit() = collectArticles.value == null && webUrlList.value == null
private fun initData() {
getCollectUrlList()
getArticles()
}
fun refresh() {
isRefreshing.value = true
webUrlList.value = null
collectArticles.value = null
checkLoginState()
}
private fun getArticles() {
collectArticles.value = collectList()
isRefreshing.value = collectArticles.value == null
}
private fun getCollectUrlList() {
async {
repo.getCollectUrls().collectLatest { response ->
when (response) {
is HttpResult.Success -> {
webUrlList.value = response.result
}
is HttpResult.Error -> {
}
}
}
}
}
private fun collectList() = repo.getCollectionList().cachedIn(viewModelScope)
fun uncollectArticle(id: Int, originId: Int) {
async {
repo.uncollectArticleById(id, originId).collectLatest { response ->
when (response) {
is HttpResult.Success -> {
}
is HttpResult.Error -> {
//收藏接口,不走success判断分支
val deleted = response.exception.message == "the result of remote's request is null"
if (deleted) {
println("取消收藏(id=$id)")
getArticles()
message.value = "已取消收藏"
}
}
}
}
}
}
@SuppressLint("NewApi")
fun deleteWebsite(id: Int) {
async {
repo.deleteWebsite(id).collectLatest { response ->
when (response) {
is HttpResult.Success -> {
}
is HttpResult.Error -> {
if (response.exception.message == "the result of remote's request is null") {
webUrlList.value?.remove(webUrlList.value?.find { it.id == id })
message.value = "删除成功"
}
}
}
}
}
}
override fun onCleared() {
super.onCleared()
println("CollectionViewModel ==> onClear")
}
}
@@ -0,0 +1,151 @@
package com.mm.hamcompose.ui.page.main.collection.edit
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.ScaffoldState
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.mm.hamcompose.R
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.*
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun WebSiteEditPage(
website: ParentBean?,
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: WebSiteEditViewModel = hiltViewModel()
) {
if (website != null) {
viewModel.webSiteTitle.value = website.name
viewModel.linkUrl.value = website.link
}
var snackLabel by remember { mutableStateOf(SNACK_ERROR) }
var index by remember { mutableStateOf(0) }
val titles by remember { viewModel.titles }
var isSaved by remember { viewModel.isSaved }
val webSiteTitle by remember { viewModel.webSiteTitle }
val linkUrl by remember { viewModel.linkUrl }
val author by remember { viewModel.author }
var errorMessage by remember { viewModel.errorMessage }
val keyboardController = LocalSoftwareKeyboardController.current
val coroutineState = rememberCoroutineScope()
if (isSaved) {
navCtrl.back()
viewModel.isSaved.value = false
}
if (errorMessage.isNotEmpty()) {
popupSnackBar(coroutineState, scaffoldState, snackLabel, errorMessage)
errorMessage = ""
}
Column(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onPress = {
keyboardController?.hide()
}
)
}
) {
HamToolBar(
title = if (website == null) "添加收藏" else "编辑收藏",
onBack = { navCtrl.back() },
rightText = stringResource(id = R.string.save),
onRightClick = {
keyboardController?.hide()
if (webSiteTitle.isNullOrEmpty()) {
snackLabel = SNACK_WARN
errorMessage = "标题不能为空"
return@HamToolBar
}
if (index == 1 && author.isNullOrEmpty()) {
snackLabel = SNACK_WARN
errorMessage = "作者不能为空"
return@HamToolBar
}
if (linkUrl.isNullOrEmpty()) {
snackLabel = SNACK_WARN
errorMessage = "链接不能为空"
return@HamToolBar
}
if (website != null) {
viewModel.saveNewCollect(-1, website.id)
} else {
viewModel.saveNewCollect(index)
}
}
)
if (website == null) {
SwitchTabBar(
titles = titles,
selectIndex = index,
onSwitchClick = { index = it })
}
LazyColumn {
item {
LabelEditView(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 20.dp),
text = webSiteTitle ?: "",
labelText = stringResource(id = R.string.title),
hintText = stringResource(id = R.string.hint_text_title),
onValueChanged = {
viewModel.webSiteTitle.value = it
},
onDeleteClick = { viewModel.webSiteTitle.value = "" },
)
}
if (index == 1) {
item {
LabelEditView(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 20.dp),
text = author ?: "",
labelText = stringResource(id = R.string.author),
hintText = stringResource(id = R.string.hint_text_name),
onValueChanged = {
viewModel.author.value = it
},
onDeleteClick = { viewModel.author.value = "" },
)
}
}
item {
LabelEditView(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 20.dp),
text = linkUrl ?: "",
labelText = stringResource(id = R.string.link),
hintText = stringResource(id = R.string.hint_text_website_url),
onValueChanged = {
viewModel.linkUrl.value = it
},
onDeleteClick = { viewModel.linkUrl.value = "" },
)
}
}
}
}
@@ -0,0 +1,88 @@
package com.mm.hamcompose.ui.page.main.collection.edit
import androidx.compose.runtime.mutableStateOf
import com.mm.hamcompose.data.bean.TabTitle
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest
import javax.inject.Inject
@HiltViewModel
class WebSiteEditViewModel @Inject constructor(
private val repo: HttpRepository
): BaseViewModel<Any>() {
var webSiteTitle = mutableStateOf<String?>(null)
var linkUrl = mutableStateOf<String?>(null)
var author = mutableStateOf<String?>(null)
var isSaved = mutableStateOf(false)
var errorMessage = mutableStateOf("")
val titles = mutableStateOf(mutableListOf(
TabTitle(601, "网站"),
TabTitle(602, "文章"),
))
override fun start() {
}
/**
* type: 0 = 添加新网站 1 = 添加新文章 -1 = 编辑网站
* id: 仅编辑网站时候用到此参数
*/
fun saveNewCollect(type: Int, id: Int = 0) {
async {
when (type) {
0 -> {
repo.addNewWebsiteCollect(webSiteTitle.value!!, linkUrl.value!!)
.collectLatest { response ->
when (response) {
is HttpResult.Success -> {
isSaved.value = true
}
is HttpResult.Error -> {
println(response.exception.message)
errorMessage.value = response.exception.message ?: "请求异常"
}
}
}
}
1 -> {
repo.addNewArticleCollect(webSiteTitle.value!!, linkUrl.value!!, author.value!!)
.collectLatest { response ->
when (response) {
is HttpResult.Success -> {
isSaved.value = true
}
is HttpResult.Error -> {
println(response.exception.message)
errorMessage.value = response.exception.message ?: "请求异常"
}
}
}
}
else -> {
repo.editCollectWebsite(id, webSiteTitle.value!!, linkUrl.value!!)
.collectLatest { response ->
when (response) {
is HttpResult.Success -> {
isSaved.value = true
}
is HttpResult.Error -> {
println(response.exception.message)
errorMessage.value = response.exception.message ?: "请求异常"
}
}
}
}
}
}
}
private fun setResponseState(errorInfo: String?) {
isSaved.value = errorInfo == "the result of remote's request is null"
}
}
@@ -0,0 +1,170 @@
package com.mm.hamcompose.ui.page.main.home
import androidx.compose.foundation.layout.Column
import androidx.compose.material.ScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import com.mm.hamcompose.ui.page.main.home.index.IndexPage
import com.mm.hamcompose.ui.page.main.home.project.ProjectPage
import com.mm.hamcompose.ui.page.main.home.square.SquarePage
import com.mm.hamcompose.ui.page.main.home.wenda.WenDaPage
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.widget.HomeSearchBar
import com.mm.hamcompose.ui.widget.TextTabBar
import kotlinx.coroutines.launch
@OptIn(ExperimentalPagerApi::class)
@Composable
fun HomePage(
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
homeIndex: Int = 0,
viewModel: HomeViewModel = hiltViewModel(),
onPageSelected: (position: Int) -> Unit,
) {
val isShowSearchBar by remember { viewModel.isShowSearchBar }
val titles by remember { viewModel.titles }
val scopeState = rememberCoroutineScope()
Column {
if (isShowSearchBar) {
HomeSearchBar(
onUserIconClick = {
navCtrl.navigate(RouteName.PROFILE) {
popUpTo(navCtrl.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
onSearchClick = {
RouteUtils.navTo(navCtrl, RouteName.ARTICLE_SEARCH + "/111")
},
onRightIconClick = {
RouteUtils.navTo(navCtrl, RouteName.GIRL_PHOTO)
}
)
}
val pagerState = rememberPagerState(
pageCount = titles.size,
initialPage = homeIndex,
initialOffscreenLimit = titles.size
)
TextTabBar(
index = pagerState.currentPage,
tabTexts = titles,
onTabSelected = { index ->
scopeState.launch {
pagerState.scrollToPage(index)
}
}
)
HorizontalPager(state = pagerState, dragEnabled = false) { page ->
onPageSelected(pagerState.currentPage)
when (page) {
0 -> IndexPage(navCtrl, scaffoldState) { viewModel.setCachePosition(0, it) }
1 -> SquarePage(navCtrl, scaffoldState) { viewModel.setCachePosition(1, it) }
2 -> ProjectPage(navCtrl, scaffoldState) { viewModel.setCachePosition(2, it) }
3 -> WenDaPage(navCtrl) { viewModel.setCachePosition(3, it) }
}
}
}
}
//@Composable
//fun DrawerMenu(
// viewModel: HomeViewModel,
// navCtrl: NavHostController? = null,
// onItemClick: (pos: Int) -> Unit,
//) {
// Column(
// modifier = Modifier
// .fillMaxWidth()
// .fillMaxHeight()
// ) {
// Box(
// modifier = Modifier
// .fillMaxWidth()
// .height(100.dp)
// .background(HamTheme.colors.themeUi)
// ) {
// Column(
// modifier = Modifier.align(Alignment.Center)
// ) {
// Image(
// painter = painterResource(id = R.drawable.ic_author),
// contentDescription = "用户头像",
// modifier = Modifier
// .size(40.dp)
// .clip(RoundedCornerShape(20.dp)),
// contentScale = ContentScale.FillBounds
// )
// }
// }
//
// viewModel.menuItems.forEachIndexed { i, item ->
// DrawerItem(
// title = item.title,
// iconRes = item.iconRes,
// navCtrl = navCtrl,
// onItemClick = {
// onItemClick(i)
// }
// )
// }
// }
//}
//
//@Composable
//private fun DrawerItem(
// title: String,
// iconRes: Int? = null,
// isSelect: Boolean = false,
// navCtrl: NavHostController? = null,
// onItemClick: () -> Unit,
//) {
// DropdownMenuItem(
// onClick = {
// onItemClick()
//
// }
// ) {
// val tintColor = if (isSelect) HamTheme.colors.themeUi else HamTheme.colors.textSecondary
// if (iconRes == null) {
// Icon(
// imageVector = Icons.Default.Home,
// contentDescription = "home",
// modifier = Modifier.size(25.dp),
// tint = tintColor
// )
// } else {
// Icon(
// painter = painterResource(id = iconRes),
// contentDescription = "home",
// modifier = Modifier.size(25.dp),
// tint = tintColor
// )
// }
//
// MediumTitle(
// title = title,
// modifier = Modifier.padding(start = 5.dp),
// color = tintColor
// )
// }
//}
@@ -0,0 +1,50 @@
package com.mm.hamcompose.ui.page.main.home
import androidx.compose.runtime.mutableStateOf
import com.mm.hamcompose.R
import com.mm.hamcompose.data.bean.HomeThemeBean
import com.mm.hamcompose.data.bean.MenuTitle
import com.mm.hamcompose.data.bean.TabTitle
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor() : BaseViewModel<HomeThemeBean>() {
var theme = mutableStateOf(HamTheme.Theme.Light)
var menuItems = mutableListOf(
MenuTitle("主页", null),
MenuTitle("福利", R.drawable.ic_menu_welfare),
MenuTitle("收藏", R.drawable.ic_star),
MenuTitle("设置", R.drawable.ic_menu_settings),
)
var isShowSearchBar = mutableStateOf(true)
val titles = mutableStateOf(
mutableListOf(
TabTitle(101, "推荐"),
TabTitle(102, "广场"),
TabTitle(103, "项目"),
TabTitle(104, "问答")
)
)
fun setCachePosition(tabIndex: Int, newPosition: Int) {
titles.value[tabIndex].cachePosition = newPosition
//val oldPosition = titles.value[tabIndex].cachePosition
//isShowSearchBar.value = newPosition <= 1
//LogUtils.w("newPosition = $newPosition isShowSearch = ${isShowSearchBar.value}")
}
override fun start() {
}
override fun onCleared() {
super.onCleared()
println("HomeViewModel ==> onClear")
}
}
@@ -0,0 +1,136 @@
package com.mm.hamcompose.ui.page.main.home.index
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ScaffoldState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemsIndexed
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.bean.WebData
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.widget.*
@OptIn(ExperimentalPagerApi::class)
@Composable
fun IndexPage(
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: IndexViewModel = hiltViewModel(),
onScrollChangeListener: (position: Int) -> Unit,
) {
viewModel.start()
val homeData = viewModel.pagingData.value?.collectAsLazyPagingItems()
val isLoaded = homeData?.loadState?.prepend?.endOfPaginationReached ?: false
val message by remember { viewModel.message }
val banners by remember { viewModel.imageList }
val refreshing by remember { viewModel.isRefreshing }
val topArticle = remember { viewModel.topArticles }
val currentPosition by remember { viewModel.currentListIndex }
val swipeRefreshState = rememberSwipeRefreshState(refreshing)
val listState = rememberLazyListState(initialFirstVisibleItemIndex = currentPosition)
val coroutineScope = rememberCoroutineScope()
if (message.isNotEmpty()) {
popupSnackBar(coroutineScope, scaffoldState, SNACK_INFO, message)
viewModel.message.value = ""
}
SwipeRefresh(
state = swipeRefreshState,
onRefresh = {
viewModel.refresh()
}
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
) {
if (banners.isNotEmpty()) {
item {
Banner(list = banners) { url, title ->
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, WebData(title, url))
}
}
}
if (topArticle.isNotEmpty()) {
itemsIndexed(topArticle) { index, item ->
MultiStateItemView(
modifier = Modifier.padding(top = if (index == 0) 5.dp else 0.dp),
data = item,
isTop = true,
onSelected = { data ->
viewModel.saveDataToHistory(item)
viewModel.savePosition(listState.firstVisibleItemIndex)
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, data)
},
onCollectClick = {
if (item.collect) {
viewModel.uncollectArticleById(it)
viewModel.topArticles[index].collect = false
} else {
viewModel.collectArticleById(it)
viewModel.topArticles[index].collect = true
}
},
onUserClick = { userId ->
RouteUtils.navTo(navCtrl, RouteName.SHARER, userId)
},
)
}
}
if (!isLoaded) {
items(5) {
MultiStateItemView(
data = Article(),
isLoading = true
)
}
} else {
if (homeData != null && homeData.itemCount > 0) {
itemsIndexed(homeData) { index, item ->
MultiStateItemView(
data = item!!,
onSelected = { data ->
viewModel.saveDataToHistory(item)
viewModel.savePosition(listState.firstVisibleItemIndex)
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, data)
},
onCollectClick = {
if (item.collect) {
viewModel.uncollectArticleById(it)
homeData.peek(index)?.collect = false
} else {
viewModel.collectArticleById(it)
homeData.peek(index)?.collect = true
}
},
onUserClick = { userId ->
RouteUtils.navTo(navCtrl, RouteName.SHARER, userId)
}
)
}
}
else {
item{ EmptyView() }
}
}
onScrollChangeListener(listState.firstVisibleItemIndex)
}
}
}
@@ -0,0 +1,114 @@
package com.mm.hamcompose.ui.page.main.home.index
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.blankj.utilcode.util.LogUtils
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.bean.BannerBean
import com.mm.hamcompose.data.db.history.HistoryDatabase
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.repository.PagingArticle
import com.mm.hamcompose.ui.page.base.BaseCollectViewModel
import com.mm.hamcompose.ui.widget.BannerData
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest
import javax.inject.Inject
@HiltViewModel
class IndexViewModel @Inject constructor(
private var repo: HttpRepository,
private val historyDb: HistoryDatabase
) : BaseCollectViewModel<BannerBean>(repo) {
var pagingData = MutableLiveData<PagingArticle>(null)
val imageList = mutableStateOf(mutableListOf<BannerData>())
var isRefreshing = mutableStateOf(false)
var topArticles = mutableStateListOf<Article>()
//列表:使用paging3分页加载框架
private fun homeData() = repo.getIndexData().cachedIn(viewModelScope)
private fun loadBanners() {
async {
repo.getBanners().collectLatest { response ->
when (response) {
is HttpResult.Success -> {
imageList.value = response.result.map {
BannerData(
imageUrl = it.imagePath ?: "",
linkUrl = it.url ?: "",
title = it.title ?: ""
)
} as MutableList<BannerData>
}
is HttpResult.Error -> {
imageList.value.clear()
}
}
}
}
}
private fun loadTopArticles() {
async {
repo.getTopArticles().collectLatest { response ->
when (response) {
is HttpResult.Success -> {
topArticles.clear()
topArticles.addAll(response.result)
}
is HttpResult.Error -> {
topArticles.clear()
}
}
}
}
}
private fun refreshBanner() {
imageList.value.clear()
loadBanners()
}
private fun refreshHots() {
topArticles.clear()
loadTopArticles()
}
private fun getHomesList() {
pagingData.value = null
pagingData.value = homeData()
isRefreshing.value = pagingData.value == null
}
fun refresh() {
resetListIndex()
isRefreshing.value = true
refreshBanner()
refreshHots()
getHomesList()
}
override fun start() {
initThat {
loadBanners()
loadTopArticles()
getHomesList()
}
}
override fun onCleared() {
LogUtils.e("IndexViewModel ===> ViewModel执行onCleared()")
super.onCleared()
}
fun saveDataToHistory(article: Article) {
cacheHistory(historyDb, article)
}
}
@@ -0,0 +1,274 @@
package com.mm.hamcompose.ui.page.main.home.project
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.material.Card
import androidx.compose.material.ScaffoldState
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemsIndexed
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.data.bean.WebData
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.theme.ListTitleHeight
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.widget.*
import com.mm.hamcompose.util.RegexUtils
private const val Newest = "最新"
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ProjectPage(
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: ProjectViewModel = hiltViewModel(),
onScrollChangeListener: (position: Int) -> Unit,
) {
viewModel.start()
val refreshing by remember { viewModel.isRefreshing }
val currentListPosition by remember { viewModel.currentListIndex }
val currentRowPosition by remember { viewModel.currentRowIndex }
val message by remember { viewModel.message }
val projects = viewModel.pagingData.value?.collectAsLazyPagingItems()
val isLoaded = projects?.loadState?.prepend?.endOfPaginationReached ?: false
val swipeRefreshState = rememberSwipeRefreshState(refreshing)
val rowListState = rememberLazyListState(currentRowPosition)
val listState = rememberLazyListState(currentListPosition)
val coroutineScope = rememberCoroutineScope()
if (message.isNotEmpty()) {
popupSnackBar(coroutineScope, scaffoldState, SNACK_INFO, message)
viewModel.message.value = ""
}
Column {
LabelList(viewModel, rowListState) { data, position ->
viewModel.saveRowPosition(rowListState.firstVisibleItemIndex)
viewModel.setupProjectId(data.id)
viewModel.triggerRefresh()
viewModel.refresh()
}
SwipeRefresh(
modifier = Modifier.fillMaxSize(),
state = swipeRefreshState,
onRefresh = {
viewModel.triggerRefresh()
viewModel.refresh()
},
) {
LazyColumn(state = listState) {
if (isLoaded) {
if (projects!!.itemCount > 0) {
itemsIndexed(projects) { index, item ->
ProjectItem(
project = item!!,
onClick = {
viewModel.savePosition(listState.firstVisibleItemIndex)
val webData = WebData(item.title, item.link!!)
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, webData)
},
onFavouriteClick = { projectId ->
if (item.collect) {
viewModel.uncollectArticleById(projectId)
projects.peek(index)?.collect = false
} else {
viewModel.collectArticleById(projectId)
projects.peek(index)?.collect = true
}
}
)
}
onScrollChangeListener(listState.firstVisibleItemIndex)
} else {
item { EmptyView() }
}
} else {
items(5) {
ProjectItem(
project = Article(),
isLoading = true
)
}
}
}
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun LabelList(
viewModel: ProjectViewModel,
listState: LazyListState,
onClick: (ParentBean, Int) -> Unit
) {
val category by remember { viewModel.list }
val tabIndex by remember { viewModel.tabIndex }
if (category.isNotEmpty()) {
if (category[0].name != Newest) {
category.add(0, ParentBean(null, id = -1, name = Newest))
}
LazyRow(
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = ListTitleHeight)
.pointerInteropFilter { false },//事件拦截,
state = listState
) {
itemsIndexed(category) { index, item ->
TextButton(
modifier = Modifier.wrapContentWidth(),
onClick = {
viewModel.setTabIndex(index)
onClick(item, index)
}
) {
Text(
text = item.name ?: "标签",
color = if (index == tabIndex) HamTheme.colors.themeUi else HamTheme.colors.textPrimary
)
}
}
}
}
}
@Composable
private fun ProjectItem(
project: Article,
onClick: () -> Unit = {},
onFavouriteClick: (id: Int) -> Unit = {},
isLoading: Boolean = false
) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(5.dp)
.clickable(enabled = !isLoading) {
onClick.invoke()
}
) {
Row {
NetworkImage(
url = project.envelopePic!!,
isLoading = isLoading,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.width(100.dp)
.fillMaxHeight()
)
Column(
modifier = Modifier
.fillMaxHeight()
.weight(1f)
.padding(vertical = 10.dp, horizontal = 5.dp)
) {
MainTitle(
title = project.title ?: "标题",
color = HamTheme.colors.textPrimary,
maxLine = 2,
modifier = Modifier.fillMaxWidth(),
isLoading = isLoading
)
TextContent(
text = project.desc ?: "内容",
maxLines = 4,
color = HamTheme.colors.textSecondary,
modifier = Modifier
.padding(top = 5.dp)
.fillMaxWidth()
.weight(1f),
isLoading = isLoading
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp)
) {
Row(
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1f)
) {
UserIcon(
modifier = Modifier.align(Alignment.CenterVertically),
isLoading = isLoading
)
MiniTitle(
text = project.author!!,
color = HamTheme.colors.textSecondary,
modifier = Modifier
.padding(start = 5.dp)
.fillMaxWidth()
.align(Alignment.CenterVertically),
isLoading = isLoading
)
}
Row(
modifier = Modifier
.padding(start = 5.dp)
.align(Alignment.CenterVertically)
.weight(1f)
) {
TimerIcon(
modifier = Modifier.align(Alignment.CenterVertically),
isLoading = isLoading
)
MiniTitle(
text = RegexUtils().timestamp(project.niceDate) ?: "",
color = HamTheme.colors.textSecondary,
modifier = Modifier
.padding(start = 5.dp)
.fillMaxWidth()
.align(Alignment.CenterVertically),
maxLines = 1,
isLoading = isLoading,
)
}
FavouriteIcon(
isFavourite = project.collect,
onClick = {
onFavouriteClick.invoke(project.id)
},
isLoading = isLoading
)
}
}
}
}
}
@@ -0,0 +1,85 @@
package com.mm.hamcompose.ui.page.main.home.project
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.mm.hamcompose.data.bean.ParentBean
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.repository.PagingArticle
import com.mm.hamcompose.ui.page.base.BaseCollectViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.onCompletion
import javax.inject.Inject
@HiltViewModel
class ProjectViewModel @Inject constructor(
private var httpRepo: HttpRepository
): BaseCollectViewModel<ParentBean>(httpRepo) {
var tabIndex = mutableStateOf(-1)
var isRefreshing = mutableStateOf(true)
var pagingData = MutableLiveData<PagingArticle?>(null)
var projectId = mutableStateOf(-1)
var currentRowIndex = mutableStateOf(0)
override fun start() {
initThat {
loadCategory()
loadContent()
}
}
fun setupProjectId(id: Int) {
this.projectId.value = id
}
/**
* 触发刷新机制
*/
fun triggerRefresh() {
isRefreshing.value = true
}
fun setTabIndex(index: Int) {
tabIndex.value = index
}
private fun loadCategory() {
async {
httpRepo.getProjectCategory()
.collectLatest { response ->
when (response) {
is HttpResult.Success -> {
list.value = response.result
tabIndex.value = 0
}
is HttpResult.Error -> {
println(response.exception.message)
}
}
}
}
}
private fun getProjects() = httpRepo.getProjects(projectId.value).cachedIn(viewModelScope)
fun refresh() {
pagingData.value = null
loadContent()
}
override fun loadContent() {
pagingData.value = getProjects()
isRefreshing.value = pagingData.value==null
}
fun saveRowPosition(position: Int) {
currentRowIndex.value = position
}
}
@@ -0,0 +1,287 @@
package com.mm.hamcompose.ui.page.main.home.search
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Icon
import androidx.compose.material.ScaffoldState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemsIndexed
import com.google.accompanist.flowlayout.FlowRow
import com.mm.hamcompose.R
import com.mm.hamcompose.data.bean.Hotkey
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.theme.ToolBarHeight
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.widget.*
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
@Composable
fun SearchPage(
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: SearchViewModel = hiltViewModel()
) {
viewModel.start()
val queries = viewModel.searches.value?.collectAsLazyPagingItems()
val hotkeys by remember { viewModel.hotkeys }
val history by remember { viewModel.history }
val searchText by remember { viewModel.searchContent }
var currentPosition by remember { viewModel.currentListIndex }
val message by remember { viewModel.message }
val keyboardCtrl = LocalSoftwareKeyboardController.current
val listState = rememberLazyListState(currentPosition)
val coroutineScope = rememberCoroutineScope()
if (message.isNotEmpty()) {
popupSnackBar(coroutineScope, scaffoldState, SNACK_INFO, message)
viewModel.message.value = ""
}
Column(Modifier.fillMaxSize()) {
SearchHead(
keyWord = searchText,
onTextChange = {
viewModel.searchContent.value = it
},
onSearchClick = {
if (it.trim().isNotEmpty()) {
currentPosition = -1
viewModel.insertKey(it)
viewModel.search(it)
}
keyboardCtrl?.hide()
},
navController = navCtrl
)
LazyColumn(state = listState) {
// part1. 搜索热词
stickyHeader {
ListTitle(title = "搜索热词")
}
item {
if (hotkeys.isNotEmpty()) {
Box {
HotkeyItem(
hotkeys = hotkeys,
viewModel = viewModel,
onSelected = { text ->
viewModel.searchContent.value = text
})
}
}
}
//part2. 历史记录
if (history.isNotEmpty()) {
stickyHeader {
ListTitle(
title = "搜索历史",
subTitle = "清空",
onSubtitleClick = {
viewModel.deleteAll()
})
}
history.forEach { item ->
item {
HistoryItem(
data = item,
viewModel = viewModel,
onSelected = {
viewModel.searchContent.value = it
}
)
}
}
}
// part3. 搜索列表
stickyHeader {
ListTitle(title = "搜索结果")
}
if (queries != null) {
itemsIndexed(queries) { index, item ->
MultiStateItemView(
data = item!!,
onSelected = { data ->
viewModel.saveDataToHistory(item)
viewModel.savePosition(listState.firstVisibleItemIndex)
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, data)
},
onCollectClick = {
if (item.collect) {
viewModel.uncollectArticleById(it)
queries.peek(index)?.collect = false
} else {
viewModel.collectArticleById(it)
queries.peek(index)?.collect = true
}
},
onUserClick = { userId ->
RouteUtils.navTo(navCtrl, RouteName.SHARER, userId)
})
}
}
}
}
}
/**
* 搜索框
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SearchHead(
keyWord: String,
onTextChange: (text: String) -> Unit,
onSearchClick: (key: String) -> Unit,
navController: NavHostController
) {
Box(
Modifier
.fillMaxWidth()
.height(ToolBarHeight)
.background(HamTheme.colors.themeUi)
) {
Row(
Modifier
.align(Alignment.Center)
.padding(10.dp)
) {
Icon(
painterResource(R.drawable.icon_back_white),
null,
Modifier
.clickable(onClick = {
navController.popBackStack()
})
.align(Alignment.CenterVertically)
.size(20.dp)
.padding(end = 10.dp),
tint = HamTheme.colors.mainColor
)
BasicTextField(
value = keyWord,
onValueChange = { onTextChange(it) },
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.height(28.dp)
.background(
color = HamTheme.colors.mainColor,
shape = RoundedCornerShape(14.dp),
)
.padding(start = 10.dp, top = 4.dp)
.align(Alignment.CenterVertically),
maxLines = 1,
singleLine = true,
textStyle = TextStyle(
color = HamTheme.colors.textSecondary,
fontWeight = FontWeight.Normal,
fontSize = 14.sp
),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(onSearch = { onSearchClick(keyWord) }),
)
MediumTitle(
title = "搜索",
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 10.dp)
.combinedClickable(onClick = { onSearchClick(keyWord) }),
color = HamTheme.colors.mainColor,
)
}
}
}
/**
* 搜索热词的item
*/
@Composable
fun HotkeyItem(
hotkeys: MutableList<Hotkey>,
viewModel: SearchViewModel,
onSelected: (key: String) -> Unit
) {
FlowRow(Modifier.padding(10.dp)) {
hotkeys.forEach {
LabelTextButton(
text = it.name ?: "",
isSelect = false,
modifier = Modifier.padding(end = 5.dp, bottom = 5.dp),
onClick = {
viewModel.savePosition(0)
onSelected(it.name!!)
viewModel.search(it.name!!)
}
)
}
}
}
/**
* 历史记录的item
*/
@Composable
fun HistoryItem(data: Hotkey, viewModel: SearchViewModel, onSelected: (key: String) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 5.dp)
) {
Icon(
painter = painterResource(id = R.drawable.ic_time),
contentDescription = "history",
modifier = Modifier.size(20.dp),
tint = HamTheme.colors.textSecondary
)
TextContent(
text = data.name ?: "",
modifier = Modifier
.weight(1f)
.padding(start = 5.dp)
.clickable {
onSelected(data.name!!)
viewModel.savePosition(0)
viewModel.search(data.name!!)
}
)
Icon(
painter = painterResource(id = R.drawable.ic_close),
contentDescription = "delete",
tint = HamTheme.colors.textSecondary,
modifier = Modifier
.size(20.dp)
.clickable {
viewModel.deleteKey(data)
})
}
}
@@ -0,0 +1,116 @@
package com.mm.hamcompose.ui.page.main.home.search
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.blankj.utilcode.util.LogUtils
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.bean.Hotkey
import com.mm.hamcompose.data.db.history.HistoryDatabase
import com.mm.hamcompose.data.db.hotkey.HotkeyDatabase
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.repository.PagingArticle
import com.mm.hamcompose.ui.page.base.BaseCollectViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class SearchViewModel @Inject constructor(
private val repo: HttpRepository,
private val hotkeyDatabase: HotkeyDatabase,
private val historyDatabase: HistoryDatabase,
): BaseCollectViewModel<Article>(repo) {
//搜索列表
val searches = MutableLiveData<PagingArticle?>(null)
//搜索词的历史记录
val history = mutableStateOf(mutableListOf<Hotkey>())
//搜索的热词
val hotkeys = mutableStateOf(mutableListOf<Hotkey>())
val searchContent = mutableStateOf("")
override fun start() {
initThat {
getHotkey()
getHistory()
}
}
fun search(key: String) {
searches.value = repo.queryArticle(key).cachedIn(viewModelScope)
}
//插入数据
fun insertKey(key: String) {
if (hasTheSame(key))
return
async {
val bean = Hotkey(link = "", name = key, order = 1, visible = 0)
hotkeyDatabase.hotkeyDao().insertHotkeys(bean)
update()
}
}
//判断是否已经存在搜索词
private fun hasTheSame(key: String): Boolean {
val same = history.value.indexOfFirst { it.name.equals(key, ignoreCase = true) } != -1
LogUtils.e("是否相同搜索词 = $same")
return same
}
//删除单条数据
fun deleteKey(key: Hotkey) {
async {
withContext(Dispatchers.IO) {
hotkeyDatabase.hotkeyDao().deleteKeys(key)
}
update()
}
}
//删除所有数据
fun deleteAll() {
async {
hotkeyDatabase.hotkeyDao().deleteAll()
update()
}
}
private fun update() = getHistory()
//查询所有数据
fun getHistory() {
async {
val result = hotkeyDatabase.hotkeyDao().loadAllKeys()
withContext(Dispatchers.Main) {
history.value = result.toMutableList()
}
}
}
fun getHotkey() {
async {
repo.getHotkeys().collectLatest { response ->
when (response) {
is HttpResult.Success -> {
hotkeys.value = response.result
}
is HttpResult.Error -> {
}
}
}
}
}
fun saveDataToHistory(article: Article) {
cacheHistory(historyDatabase, article)
}
}
@@ -0,0 +1,102 @@
package com.mm.hamcompose.ui.page.main.home.square
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemsIndexed
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.widget.EmptyView
import com.mm.hamcompose.ui.widget.MultiStateItemView
import com.mm.hamcompose.ui.widget.SNACK_INFO
import com.mm.hamcompose.ui.widget.popupSnackBar
@Composable
fun SquarePage(
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: SquareViewModel = hiltViewModel(),
onScrollChangeListener: (position: Int) -> Unit,
) {
viewModel.start()
val squareData = viewModel.pagingData.value?.collectAsLazyPagingItems()
val isLoaded = squareData?.loadState?.prepend?.endOfPaginationReached ?: false
val refreshing: Boolean by remember { viewModel.isRefreshing }
val swipeRefreshState = rememberSwipeRefreshState(refreshing)
val currentPosition by remember { viewModel.currentListIndex }
val message by remember { viewModel.message }
val listState = rememberLazyListState(currentPosition)
val coroutineScope = rememberCoroutineScope()
if (message.isNotEmpty()) {
popupSnackBar(coroutineScope, scaffoldState, SNACK_INFO, message)
viewModel.message.value = ""
}
SwipeRefresh(
state = swipeRefreshState,
onRefresh = { viewModel.refresh() }
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
contentPadding = PaddingValues(top = 10.dp)
) {
if (isLoaded) {
if (squareData!!.itemCount > 0) {
itemsIndexed(squareData) { index, item ->
MultiStateItemView(
data = item!!,
onSelected = {
viewModel.saveDataToHistory(item)
viewModel.savePosition(listState.firstVisibleItemIndex)
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, it)
},
onCollectClick = {
if (item.collect) {
viewModel.uncollectArticleById(it)
squareData.peek(index)?.collect = false
} else {
viewModel.collectArticleById(it)
squareData.peek(index)?.collect = true
}
},
onUserClick = { userId ->
RouteUtils.navTo(navCtrl, RouteName.SHARER, userId)
})
}
onScrollChangeListener(listState.firstVisibleItemIndex)
} else {
item { EmptyView() }
}
} else {
items(5) {
MultiStateItemView(
data = Article(),
isLoading = true
)
}
}
}
}
}
@@ -0,0 +1,51 @@
package com.mm.hamcompose.ui.page.main.home.square
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.blankj.utilcode.util.LogUtils
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.db.history.HistoryDatabase
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.repository.PagingArticle
import com.mm.hamcompose.ui.page.base.BaseCollectViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class SquareViewModel @Inject constructor(
private var repo: HttpRepository,
private val db: HistoryDatabase,
): BaseCollectViewModel<Article>(repo) {
var pagingData = MutableLiveData<PagingArticle?>(null)
var isRefreshing = mutableStateOf(false)
override fun start() {
initThat {
pagingData.value = squareData()
}
}
fun refresh() {
resetListIndex()
isRefreshing.value = true
pagingData.value = null
pagingData.value = squareData()
isRefreshing.value = pagingData.value==null
}
private fun squareData() = repo.getSquareData().cachedIn(viewModelScope)
fun saveDataToHistory(article: Article) {
cacheHistory(db, article)
}
override fun onCleared() {
LogUtils.e("SquareViewModel ===> ViewModel执行onCleared()")
super.onCleared()
}
}
@@ -0,0 +1,150 @@
package com.mm.hamcompose.ui.page.main.home.wenda
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Card
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.bean.WebData
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.widget.*
import com.mm.hamcompose.util.RegexUtils
@Composable
fun WenDaPage(
navCtrl: NavHostController,
viewModel: WenDaViewModel = hiltViewModel(),
onScrollChangeListener: (position: Int) -> Unit,
) {
viewModel.start()
val wendaData = viewModel.pagingData.value?.collectAsLazyPagingItems()
val isLoaded = wendaData?.loadState?.prepend?.endOfPaginationReached ?: false
val refreshing by remember { viewModel.isRefreshing }
val currentPosition by remember { viewModel.currentListIndex }
val swipeRefreshState = rememberSwipeRefreshState(refreshing)
val listState = rememberLazyListState(currentPosition)
SwipeRefresh(
state = swipeRefreshState,
onRefresh = { viewModel.refresh() }
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
contentPadding = PaddingValues(vertical = 5.dp)
) {
if (isLoaded) {
if (wendaData != null && wendaData.itemCount > 0) {
items(wendaData) { wenda ->
WenDaItem(wenda!!) {
viewModel.savePosition(listState.firstVisibleItemIndex)
wenda.run {
RouteUtils.navTo(
navCtrl,
RouteName.WEB_VIEW,
WebData(title, link!!)
)
}
}
}
onScrollChangeListener(listState.firstVisibleItemIndex)
} else {
item { EmptyView() }
}
} else {
items(5) {
WenDaItem(article = Article(), isLoading = true)
}
}
}
}
}
@Composable
private fun WenDaItem(article: Article, isLoading: Boolean = false, onClick: () -> Unit = {}) {
Card(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(5.dp)
.clickable(enabled = !isLoading) {
onClick.invoke()
}
) {
Column(
modifier = Modifier.padding(10.dp)
) {
//标题
MainTitle(
title = titleSubstring(article.title) ?: "每日一问",
maxLine = 2,
isLoading = isLoading,
modifier = Modifier.fillMaxWidth()
)
Row(
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.padding(vertical = 5.dp)
) {
//UserIcon(isLoading = isLoading)
MiniTitle(
text = "作者:${article.author ?: "xxx"}",
color = HamTheme.colors.textSecondary,
modifier = Modifier
.padding(start = if (isLoading) 5.dp else 0.dp)
.align(Alignment.CenterVertically),
isLoading = isLoading
)
Spacer(modifier = Modifier.width(10.dp))
//发布时间
//TimerIcon(isLoading = isLoading)
MiniTitle(
modifier = Modifier
.padding(start = if (isLoading) 5.dp else 0.dp)
.align(Alignment.CenterVertically),
text = "日期:${RegexUtils().timestamp(article.niceDate) ?: "2020"}",
color = HamTheme.colors.textSecondary,
maxLines = 1,
isLoading = isLoading
)
}
TextContent(
text = RegexUtils().symbolClear(article.desc),
maxLines = 3,
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 60.dp),
isLoading = isLoading
)
}
}
}
private fun titleSubstring(oldText: String?): String? {
return oldText?.run {
var newText = this
if (startsWith("每日一问") && contains(" | ")) {
newText = substring(indexOf(" | ")+3, length)
}
newText
}
}
@@ -0,0 +1,36 @@
package com.mm.hamcompose.ui.page.main.home.wenda
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.repository.PagingArticle
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class WenDaViewModel @Inject constructor(
private var repo: HttpRepository
): BaseViewModel<Article>() {
var pagingData = MutableLiveData<PagingArticle?>(null)
var isRefreshing = mutableStateOf(false)
override fun start() {
initThat { pagingData.value = squareData() }
}
fun refresh() {
resetListIndex()
isRefreshing.value = true
pagingData.value = null
pagingData.value = squareData()
isRefreshing.value = pagingData.value==null
}
private fun squareData() = repo.getWendaData().cachedIn(viewModelScope)
}
@@ -0,0 +1,504 @@
package com.mm.hamcompose.ui.page.main.profile
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import com.google.accompanist.placeholder.material.placeholder
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.mm.hamcompose.R
import com.mm.hamcompose.data.bean.MY_USER_ID
import com.mm.hamcompose.data.bean.PointsBean
import com.mm.hamcompose.data.bean.UserInfo
import com.mm.hamcompose.data.bean.WebData
import com.mm.hamcompose.theme.*
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.widget.*
private const val MAX_SIZE = 10
@Composable
fun ProfilePage(
navCtrl: NavHostController,
viewModel: ProfileViewModel = hiltViewModel()
) {
viewModel.start()
val isLogin by remember { viewModel.isLogin }
val messageCount by remember { viewModel.messageCount }
var clickJoinUs by remember { mutableStateOf(false) }
val refreshState = rememberSwipeRefreshState(viewModel.isRefresh.value)
val listState = rememberLazyListState()
Column(
modifier = Modifier
.padding(bottom = BottomNavBarHeight)
.fillMaxSize()
.background(color = HamTheme.colors.mainColor)
) {
if (isLogin && viewModel.userInfo.value != null) {
SwipeRefresh(
state = refreshState,
onRefresh = { viewModel.refresh() }
) {
LazyColumn(state = listState) {
item {
HeaderPart(
navCtrl = navCtrl,
viewModel = viewModel,
messageCount = messageCount
)
}
item {
ContentPart(
navCtrl = navCtrl,
viewModel = viewModel,
modifier = Modifier.defaultMinSize(minHeight = 200.dp)
)
}
item {
FooterPart(
navCtrl = navCtrl,
messageCount = messageCount,
onJoinUsClick = {
clickJoinUs = true
})
}
}
}
} else {
HamToolBar(title = "我的")
EmptyView(
tips = "点击登录",
imageVector = Icons.Default.Face
) {
RouteUtils.navTo(navCtrl, RouteName.LOGIN)
}
}
}
if (clickJoinUs) {
InfoDialog(
title = "加入我们",
content = arrayOf(
"开源作者: 鸿洋",
"作者博客: https://blog.csdn.net/lmj623565791",
"公众号 https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzAxMTI4MTkwNQ==&scene=124#wechat_redirect",
"QQ交流群: 591683946",
),
onDismiss = {
clickJoinUs = false
}
)
}
}
//用户信息
@Composable
fun HeaderPart(
navCtrl: NavHostController,
viewModel: ProfileViewModel,
messageCount: Int,
) {
val userInfo by remember { viewModel.userInfo }
val myPoints by remember { viewModel.myPoints }
Column {
ProfileToolBar(
msgCount = messageCount,
onMessageIconClick = {
RouteUtils.navTo(navCtrl, RouteName.MESSAGE)
},
onDeleteIconClick = {
RouteUtils.navTo(navCtrl, RouteName.SETTINGS)
},
onDashboardIconClick = {
RouteUtils.navTo(navCtrl, RouteName.SETTINGS)
},
)
UserInfoItem(myPoints, userInfo)
UserOptionsItem(
onCollectClick = {
try {
navCtrl.navigate(RouteName.COLLECTION) {
popUpTo(navCtrl.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
} catch (ex: Exception) {
ex.printStackTrace()
}
},
onMyShareClick = {
RouteUtils.navTo(navCtrl, RouteName.SHARER, MY_USER_ID)
},
onHistoryClick = {
RouteUtils.navTo(navCtrl, RouteName.HISTORY)
},
onRankingClick = {
RouteUtils.navTo(navCtrl, RouteName.RANKING)
}
)
}
}
//我的文章
@OptIn(ExperimentalAnimationApi::class, ExperimentalFoundationApi::class)
@Composable
fun ContentPart(navCtrl: NavHostController, viewModel: ProfileViewModel, modifier: Modifier) {
val myArticles by remember { viewModel.myArticles }
Box(
modifier = modifier
.fillMaxSize()
.padding(top = 20.dp)
) {
if (myArticles.isNotEmpty()) {
Column {
ListTitle(title = "我的文章")
var newList = myArticles
if (myArticles.size > MAX_SIZE) {
newList = myArticles.subList(0, 9)
}
newList.forEachIndexed { index, article ->
TextContent(
text = "${index + 1}. ${article.title}",
modifier = Modifier
.padding(
start = 10.dp,
end = 10.dp,
bottom = 10.dp,
top = if (index == 0) 10.dp else 0.dp
)
.clickable {
RouteUtils.navTo(
navCtrl = navCtrl,
destinationName = RouteName.WEB_VIEW,
args = WebData(article.title, article.link ?: "")
)
},
)
}
}
} else {
ListTitle(title = "我的文章")
Column(
modifier = Modifier
.padding(top = ListTitleHeight)
.align(Alignment.Center)
.clickable {
RouteUtils.navTo(navCtrl, RouteName.SHARE_ARTICLE)
}
.animateContentSize()
) {
AddIcon(
modifier = Modifier
.padding(bottom = 5.dp)
.align(Alignment.CenterHorizontally),
color = HamTheme.colors.textSecondary
)
TextContent(text = "添加文章")
}
}
}
}
//基本操作
@Composable
fun FooterPart(
navCtrl: NavHostController,
messageCount: Int,
onJoinUsClick: () -> Unit,
) {
Column(
modifier = Modifier.padding(bottom = 10.dp)
) {
ArrowRightListItem(
iconRes = painterResource(R.drawable.ic_message),
title = "消息",
msgCount = messageCount
) {
RouteUtils.navTo(navCtrl, RouteName.MESSAGE)
}
ArrowRightListItem(
iconRes = painterResource(R.drawable.ic_menu_settings),
title = "设置"
) {
RouteUtils.navTo(navCtrl, RouteName.SETTINGS)
}
ArrowRightListItem(
iconRes = painterResource(R.drawable.ic_feedback),
title = "WanAndroid"
) {
RouteUtils.navTo(
navCtrl = navCtrl,
destinationName = RouteName.WEB_VIEW,
args = WebData(title = "官方网站", url = "https://www.wanandroid.com/index")
)
}
ArrowRightListItem(
iconRes = painterResource(R.drawable.ic_data),
title = "积分规则"
) {
RouteUtils.navTo(
navCtrl = navCtrl,
destinationName = RouteName.WEB_VIEW,
args = WebData(title = "积分规则", url = "https://www.wanandroid.com/blog/show/2653")
)
}
ArrowRightListItem(
iconRes = painterResource(R.drawable.ic_community),
title = "加入我们"
) {
onJoinUsClick.invoke()
}
}
}
//调色板、清理缓存、消息的ToolBar
@Composable
private fun ProfileToolBar(
msgCount: Int,
onMessageIconClick: () -> Unit,
onDeleteIconClick: () -> Unit,
onDashboardIconClick: () -> Unit,
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(ToolBarHeight)
.background(HamTheme.colors.themeUi)
) {
Row(modifier = Modifier.align(Alignment.CenterEnd)) {
Box(
modifier = Modifier.wrapContentSize()
) {
NotificationIcon(
modifier = Modifier
.size(24.dp)
.clickable { onMessageIconClick.invoke() }
)
if (msgCount > 0) {
DotView(modifier = Modifier.align(Alignment.TopEnd))
}
}
Icon(
Icons.Default.Delete,
contentDescription = "Clear cache",
modifier = Modifier
.padding(start = 10.dp)
.clickable {
onDeleteIconClick.invoke()
},
tint = white
)
Icon(
painter = painterResource(id = R.drawable.ic_theme),
contentDescription = "Switch theme",
modifier = Modifier
.padding(horizontal = 10.dp)
.clickable {
onDashboardIconClick.invoke()
},
tint = white
)
}
}
}
//用户基本信息
@Composable
private fun UserInfoItem(myPoints: PointsBean?, userInfo: UserInfo?) {
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.background(HamTheme.colors.themeUi)
) {
val (icon, info) = createRefs()
Image(
painter = painterResource(id = R.drawable.wukong),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(start = 20.dp)
.width(48.dp)
.height(48.dp)
.clip(RoundedCornerShape(24.dp))
.placeholder(
visible = userInfo == null,
color = HamTheme.colors.placeholder
)
.constrainAs(icon) {
top.linkTo(parent.top)
start.linkTo(parent.start)
},
)
Column(
modifier = Modifier
.padding(start = 10.dp, bottom = 20.dp)
.constrainAs(info) {
start.linkTo(icon.end)
top.linkTo(parent.top)
}
) {
MainTitle(
title = userInfo?.username ?: "",
modifier = Modifier.placeholder(
visible = userInfo == null,
color = HamTheme.colors.placeholder
)
)
if (userInfo?.email?.isNotEmpty() == true) {
MiniTitle(
text = "email: ${userInfo.email}",
modifier = Modifier.padding(top = 5.dp)
)
}
Row(
modifier = Modifier.padding(top = 5.dp)
) {
TagView(
tagText = "Lv${myPoints?.level ?: "99"}",
tagBgColor = HamTheme.colors.themeUi,
borderColor = HamTheme.colors.textSecondary,
isLoading = myPoints == null
)
TagView(
modifier = Modifier.padding(start = 5.dp),
tagText = "积分${myPoints?.coinCount ?: "10086"}",
tagBgColor = HamTheme.colors.themeUi,
borderColor = HamTheme.colors.textSecondary,
isLoading = myPoints == null
)
}
}
}
}
//我的收藏、我的分享、历史记录、积分排行榜
@Composable
private fun UserOptionsItem(
onCollectClick: () -> Unit,
onMyShareClick: () -> Unit,
onHistoryClick: () -> Unit,
onRankingClick: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = 20.dp)
) {
ProfileOptionItem(
modifier = Modifier.weight(1f),
title = "我的收藏",
iconRes = Icons.Default.FavoriteBorder
) {
onCollectClick.invoke()
}
ProfileOptionItem(
modifier = Modifier.weight(1f),
title = "我的文章",
iconRes = painterResource(R.drawable.ic_article)
) {
onMyShareClick.invoke()
}
ProfileOptionItem(
modifier = Modifier.weight(1f),
title = "历史浏览",
iconRes = painterResource(R.drawable.ic_history_record)
) {
onHistoryClick.invoke()
}
ProfileOptionItem(
modifier = Modifier.weight(1f),
title = "积分排行",
iconRes = painterResource(R.drawable.ic_ranking)
) {
onRankingClick.invoke()
}
}
}
@Composable
private fun ProfileOptionItem(
modifier: Modifier,
title: String,
iconRes: Any,
onClick: () -> Unit
) {
Column(modifier = modifier.clickable { onClick() }) {
when (iconRes) {
is Painter -> {
Icon(
painter = iconRes,
contentDescription = null,
tint = HamTheme.colors.icon,
modifier = Modifier
.size(24.dp)
.align(Alignment.CenterHorizontally)
)
}
is ImageVector -> {
Icon(
imageVector = iconRes,
contentDescription = null,
tint = HamTheme.colors.icon,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}
MiniTitle(
text = title,
modifier = Modifier
.padding(top = 5.dp)
.align(Alignment.CenterHorizontally)
)
}
}
@@ -0,0 +1,124 @@
package com.mm.hamcompose.ui.page.main.profile
import androidx.compose.runtime.mutableStateOf
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.bean.PointsBean
import com.mm.hamcompose.data.bean.UserInfo
import com.mm.hamcompose.data.db.user.UserInfoDatabase
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val repo: HttpRepository,
private val db: UserInfoDatabase
) : BaseViewModel<Article>() {
val isLogin = mutableStateOf(false)
val isRefresh = mutableStateOf(false)
val messageCount = mutableStateOf(0)
var userInfo = mutableStateOf<UserInfo?>(null)
var page = mutableStateOf(1)
var myPoints = mutableStateOf<PointsBean?>(null)
val myArticles = mutableStateOf(mutableListOf<Article>())
override fun start() {
checkLoginState()
}
private fun initUserRemoteData() {
initMessageCount()
initBasicUserInfo()
getMyShareArticles()
}
fun refresh() {
isRefresh.value = true
checkLoginState()
}
private fun initBasicUserInfo() {
async {
repo.getBasicUserInfo().collectLatest { response ->
when (response) {
is HttpResult.Success -> {
myPoints.value = response.result.coinInfo
userInfo.value = response.result.userInfo
insertNewestUserInfo(response.result.userInfo)
}
is HttpResult.Error -> {
println(response.exception.message)
}
}
}
}
}
private suspend fun insertNewestUserInfo(user: UserInfo) {
withContext(Dispatchers.IO) {
db.userInfoDao().insertUserInfo(user)
}
}
private fun initMessageCount() {
async {
repo.getMessageCount().collectLatest { response ->
when (response) {
is HttpResult.Success -> {
messageCount.value = response.result
}
is HttpResult.Error -> {
println(response.exception.message)
}
}
}
}
}
private fun checkLoginState() {
async {
flow { emit(db.userInfoDao().queryUserInfo()) }
.flowOn(Dispatchers.IO)
.collectLatest { users ->
isLogin.value = users.isNotEmpty()
if (users.isNotEmpty()) {
userInfo.value = users[0]
initUserRemoteData()
}
}
}
}
private fun isNotInit() = userInfo.value==null && myPoints.value==null
private fun getMyShareArticles() {
async {
repo.getMyShareArticles(page.value).collectLatest { response ->
isRefresh.value = false
when (response) {
is HttpResult.Success -> {
myPoints.value = response.result.coinInfo
myArticles.value = response.result.shareArticles.datas
}
is HttpResult.Error -> {
println(response.exception.message)
}
}
}
}
}
override fun onCleared() {
super.onCleared()
println("ProfileViewModel ==> onClear")
}
}
@@ -0,0 +1,82 @@
package com.mm.hamcompose.ui.page.main.profile.history
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.ScaffoldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.mm.hamcompose.data.bean.WebData
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.*
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HistoryPage(
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: HistoryViewModel = hiltViewModel()
) {
viewModel.start()
val historyList by remember { viewModel.list }
val isClear by remember { viewModel.isClear }
val asyncScope = rememberCoroutineScope()
if (isClear) {
popupSnackBar(asyncScope, scaffoldState, SNACK_INFO, "历史记录已清空")
viewModel.isClear.value = false
}
Column(
modifier = Modifier.fillMaxSize()
) {
HamToolBar(
title = "历史浏览记录",
rightText = "清除所有",
onBack = { navCtrl.back() },
onRightClick = {
viewModel.clearAllHistory()
}
)
ListTitle(title = "最近在看", modifier = Modifier.padding(top = 12.dp, bottom = 5.dp))
if (historyList.isNotEmpty()) {
LazyColumn {
itemsIndexed(historyList) { index, item ->
TextContent(
text = "${index + 1}. ${item.title}",
modifier = Modifier
.padding(horizontal = 10.dp, vertical = 5.dp)
.clickable {
RouteUtils.navTo(
navCtrl = navCtrl,
destinationName = RouteName.WEB_VIEW,
args = WebData(item.title, item.link)
)
},
maxLines = 2,
)
}
}
} else {
EmptyView(tips = "暂无浏览记录", imageVector = Icons.Default.Info)
}
}
}
@@ -0,0 +1,40 @@
package com.mm.hamcompose.ui.page.main.profile.history
import androidx.compose.runtime.mutableStateOf
import com.mm.hamcompose.data.bean.HistoryRecord
import com.mm.hamcompose.data.db.history.HistoryDatabase
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class HistoryViewModel @Inject constructor(private val db: HistoryDatabase): BaseViewModel<HistoryRecord>() {
var isClear = mutableStateOf(false)
override fun start() {
initThat { getHistoryList() }
}
private fun getHistoryList() {
async {
val history = withContext(Dispatchers.IO) { db.historyDao().queryAll() }
withContext(Dispatchers.Main) {
list.value = history.toMutableList()
}
}
}
fun clearAllHistory() {
async {
withContext(Dispatchers.IO) { db.historyDao().deleteAll() }
withContext(Dispatchers.Main) {
list.value = mutableListOf()
isClear.value = true
}
}
}
}
@@ -0,0 +1,81 @@
package com.mm.hamcompose.ui.page.main.profile.message
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.runtime.*
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.EmptyView
import com.mm.hamcompose.ui.widget.HamToolBar
import com.mm.hamcompose.ui.widget.SwitchTabBar
import kotlinx.coroutines.launch
@OptIn(ExperimentalPagerApi::class)
@Composable
fun MessagePage(
navCtrl: NavHostController,
viewModel: MessageViewModel = hiltViewModel()
) {
val titles by remember { viewModel.titles }
val tabIndex by remember { viewModel.tabIndex }
val unreadMessages = viewModel.pagingUnread.value?.collectAsLazyPagingItems()
val readedMessages = viewModel.pagingReaded.value?.collectAsLazyPagingItems()
Column {
HamToolBar(title = "我的消息", onBack = { navCtrl.back() })
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState(
pageCount = titles.size,
initialPage = tabIndex,
initialOffscreenLimit = titles.size
)
SwitchTabBar(
titles = titles,
selectIndex = tabIndex,
) {
viewModel.tabIndex.value = it
coroutineScope.launch {
pagerState.scrollToPage(tabIndex)
}
}
HorizontalPager(state = pagerState) { page ->
viewModel.tabIndex.value = pagerState.currentPage
when (page) {
0 -> MessageScreen(unreadMessages, false) {
viewModel.refreshUnreadData()
}
1 -> MessageScreen(readedMessages, true) {
viewModel.refreshReadedData()
}
}
}
}
}
@Composable
private fun MessageScreen(data: LazyPagingItems<Any>?, isReaded: Boolean, onRefresh: ()-> Unit) {
if (data == null) {
EmptyView(
tips = if (isReaded) "没有已读消息" else "没有未读消息",
imageVector = Icons.Default.Refresh,
onClick = onRefresh)
} else {
LazyColumn {
//TODO 未知消息的数据json
}
}
}
@@ -0,0 +1,48 @@
package com.mm.hamcompose.ui.page.main.profile.message
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.mm.hamcompose.data.bean.TabTitle
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.repository.PagingAny
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class MessageViewModel @Inject constructor(private val repo: HttpRepository): BaseViewModel<Any>() {
var tabIndex = mutableStateOf(0)
var errorMessage = mutableStateOf<String?>(null)
var pagingUnread = MutableLiveData<PagingAny?>(null)
var pagingReaded = MutableLiveData<PagingAny?>(null)
val titles = mutableStateOf(mutableListOf(
TabTitle(401, "未读消息"),
TabTitle(402, "已读消息"),
))
override fun start() {
initThat {
pagingUnread.value = unread()
pagingReaded.value = readed()
}
}
fun refreshUnreadData() {
pagingUnread.value = null
pagingUnread.value = unread()
}
fun refreshReadedData() {
pagingReaded.value = null
pagingReaded.value = readed()
}
private fun unread() = repo.getUnreadMessages().cachedIn(viewModelScope)
private fun readed() = repo.getReadedMessages().cachedIn(viewModelScope)
}
@@ -0,0 +1,75 @@
package com.mm.hamcompose.ui.page.main.profile.points
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.mm.hamcompose.data.bean.PointsBean
import com.mm.hamcompose.data.bean.TabTitle
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.repository.PagingPoints
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest
import javax.inject.Inject
@HiltViewModel
class PointsRankingViewModel @Inject constructor(
private val repo: HttpRepository,
) : BaseViewModel<PointsBean>() {
val pagingRanking = MutableLiveData<PagingPoints?>(null)
val pagingRecords = MutableLiveData<PagingPoints?>(null)
val personalPoints = mutableStateOf<PointsBean?>(null)
var tabIndex = mutableStateOf(0)
var errorMessage = mutableStateOf<String?>(null)
val titles = mutableStateOf(
mutableListOf(
TabTitle(501, "排行榜"),
TabTitle(502, "我的积分"),
)
)
override fun start() {
initThat {
fetchData()
}
}
private fun fetchData() {
if (personalPoints.value == null) {
requestPersonPoints()
}
if (pagingRanking.value == null) {
pagingRanking.value = ranking()
}
if (pagingRecords.value == null) {
pagingRecords.value = records()
}
}
private fun requestPersonPoints() {
async {
repo.getMyPointsRanking()
.collectLatest { response ->
when (response) {
is HttpResult.Success -> {
personalPoints.value = response.result
}
is HttpResult.Error -> {
errorMessage.value = response.exception.message
}
}
}
}
}
private fun ranking(): PagingPoints {
return repo.getPointsRankings().cachedIn(viewModelScope)
}
private fun records() = repo.getPointsRecords().cachedIn(viewModelScope)
}
@@ -0,0 +1,286 @@
package com.mm.hamcompose.ui.page.main.profile.points
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import androidx.paging.compose.itemsIndexed
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import com.mm.hamcompose.R
import com.mm.hamcompose.data.bean.PointsBean
import com.mm.hamcompose.theme.H5
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.*
import kotlinx.coroutines.launch
@OptIn(ExperimentalPagerApi::class)
@Composable
fun PointsRankingPage(
navCtrl: NavHostController,
viewModel: PointsRankingViewModel = hiltViewModel()
) {
viewModel.start()
val titles by remember { viewModel.titles }
val rankings = viewModel.pagingRanking.value?.collectAsLazyPagingItems()
val isLoaded = rankings?.loadState?.prepend?.endOfPaginationReached ?: false
val records = viewModel.pagingRecords.value?.collectAsLazyPagingItems()
val personPoints by remember { viewModel.personalPoints }
val tabIndex by remember { viewModel.tabIndex }
Column(
modifier = Modifier
.fillMaxSize()
.background(color = HamTheme.colors.themeUi)
) {
HamToolBar(title = "积分排行", onBack = { navCtrl.back() })
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState(
pageCount = titles.size,
initialPage = tabIndex,
initialOffscreenLimit = titles.size
)
SwitchTabBar(titles = titles, selectIndex = tabIndex, heightValue = 30.dp) {
viewModel.tabIndex.value = it
coroutineScope.launch {
pagerState.scrollToPage(tabIndex)
}
}
HorizontalPager(state = pagerState) { page ->
viewModel.tabIndex.value = pagerState.currentPage
when (page) {
0 -> RankingScreen(isLoaded, rankings, personPoints, navCtrl)
1 -> RecordsScreen(records, personPoints?.coinCount)
}
}
}
}
@Composable
private fun RankingScreen(
isLoaded: Boolean,
rankings: LazyPagingItems<PointsBean>?,
person: PointsBean?,
navCtrl: NavHostController
) {
Column(
modifier = Modifier
.padding(vertical = 20.dp, horizontal = 10.dp)
.fillMaxSize()
.background(
color = HamTheme.colors.mainColor,
shape = RoundedCornerShape(5.dp)
)
.padding(20.dp)
) {
Row(
modifier = Modifier.padding(bottom = 10.dp)
) {
MediumTitle(
title = "用户",
modifier = Modifier.weight(1f),
)
MediumTitle(
title = "积分",
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center
)
MediumTitle(
title = "排名",
modifier = Modifier.weight(1f),
textAlign = TextAlign.End
)
}
Divider(
thickness = 0.5.dp,
modifier = Modifier.padding(top = 5.dp, bottom = 10.dp)
)
if (person != null) {
Row(
modifier = Modifier.padding(bottom = 10.dp)
) {
TextContent(
text = person.username,
modifier = Modifier.weight(1f),
color = HamTheme.colors.themeUi,
maxLines = 1,
)
TextContent(
text = person.coinCount,
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center,
color = HamTheme.colors.themeUi,
maxLines = 1,
)
TextContent(
text = person.rank!!,
modifier = Modifier.weight(1f),
textAlign = TextAlign.End,
color = HamTheme.colors.themeUi,
maxLines = 1,
)
}
}
if (!isLoaded) {
Box(modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.size(48.dp),
color = HamTheme.colors.themeUi
)
}
} else {
if (rankings != null) {
LazyColumn {
itemsIndexed(rankings) { index, rank ->
Row(
modifier = Modifier
.padding(bottom = 10.dp)
.clickable {
RouteUtils.navTo(navCtrl, RouteName.SHARER, rank?.userId)
}
) {
Row(
modifier = Modifier.weight(1f),
) {
TextContent(
text = rank?.username ?: "username",
color = if (index < 3) HamTheme.colors.textPrimary else HamTheme.colors.textSecondary,
maxLines = 1,
)
if (index < 3) {
Icon(
painter = painterResource(id = R.drawable.ic_hot),
contentDescription = null,
tint = HamTheme.colors.hot,
modifier = Modifier.size(16.dp)
)
}
}
TextContent(
text = rank?.coinCount ?: "points",
textAlign = TextAlign.Center,
color = if (index < 3) HamTheme.colors.textPrimary else HamTheme.colors.textSecondary,
maxLines = 1,
)
TextContent(
text = rank?.rank ?: "ranking",
modifier = Modifier.weight(1f),
textAlign = TextAlign.End,
color = if (index < 3) HamTheme.colors.textPrimary else HamTheme.colors.textSecondary,
maxLines = 1,
)
}
}
}
}
else {
EmptyView()
}
}
}
}
@Composable
private fun RecordsScreen(
records: LazyPagingItems<PointsBean>?,
points: String?,
) {
Column(
modifier = Modifier
.padding(vertical = 20.dp, horizontal = 10.dp)
.fillMaxSize()
.background(
color = HamTheme.colors.mainColor,
shape = RoundedCornerShape(5.dp)
)
.padding(20.dp)
) {
Row(
modifier = Modifier.padding(bottom = 20.dp)
) {
MediumTitle(title = "积分详情")
MiniTitle(
text = "合计:${points ?: 0}",
Modifier
.padding(start = 5.dp)
.align(Alignment.Bottom)
)
}
if (records == null) {
return
}
LazyColumn {
items(records) { record ->
Column {
Row {
TextContent(
text = record?.desc!!,
modifier = Modifier.weight(1f),
color = when (record.type) {
1 -> HamTheme.colors.info
2 -> HamTheme.colors.success
else -> HamTheme.colors.hot
}
)
TextContent(
text = record.reason!!,
color = when (record.type) {
1 -> HamTheme.colors.info
2 -> HamTheme.colors.success
else -> HamTheme.colors.hot
},
modifier = Modifier.padding(start = 10.dp)
)
}
Divider(
thickness = 0.5.dp,
modifier = Modifier.padding(top = 5.dp, bottom = 10.dp)
)
}
}
}
}
}
@@ -0,0 +1,172 @@
package com.mm.hamcompose.ui.page.main.profile.settings
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.ScaffoldState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.blankj.utilcode.util.SPUtils
import com.mm.hamcompose.R
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.theme.THEME_COLOR_KEY
import com.mm.hamcompose.theme.themeColors
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.*
import com.mm.hamcompose.util.CacheDataManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun SettingsPage(
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
//onThemeSelect: (color: Color)-> Unit,
viewModel: SettingsViewModel = hiltViewModel(),
) {
val context = LocalContext.current
var clickExitApp by remember { mutableStateOf(false) }
var logout by remember { viewModel.logout }
var isClearCache by remember { mutableStateOf(false) }
var isAboutMe by remember { mutableStateOf(false) }
var clickPalette by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
var cacheSize by remember { mutableStateOf("") }
var tipsMessage by remember { mutableStateOf("") }
val themeIndex by remember { viewModel.themeIndex }
val selectTheme by remember { viewModel.selectTheme }
if (selectTheme!=null) {
HamTheme.colors.themeUi = selectTheme!!
HamTheme.colors.primaryBtnBg = selectTheme!!
viewModel.selectTheme.value = null
}
if (logout) {
println("SettingsPage ===> logout")
navCtrl.popBackStack()
viewModel.logout.value = false
}
coroutineScope.launch {
val size = withContext(Dispatchers.IO) {
CacheDataManager.getTotalCacheSize(context)
}
cacheSize = withContext(Dispatchers.Main) { size }
}
if (tipsMessage.isNotEmpty()) {
popupSnackBar(coroutineScope, scaffoldState, SNACK_INFO, tipsMessage)
tipsMessage = ""
}
Column(Modifier.fillMaxSize()) {
HamToolBar(title = "设置", onBack = { navCtrl.back() })
LazyColumn {
item {
ArrowRightListItem(
iconRes = painterResource(R.drawable.ic_theme),
title = "主题更换",
) {
clickPalette = true
}
ArrowRightListItem(
iconRes = painterResource(R.drawable.ic_message),
title = "清理缓存",
valueText = cacheSize
) {
isClearCache = true
}
ArrowRightListItem(
iconRes = painterResource(R.drawable.ic_help),
title = "关于我"
) {
isAboutMe = true
}
ArrowRightListItem(
iconRes = painterResource(R.drawable.ic_message),
title = "退出登录",
) {
clickExitApp = true
}
}
}
}
if (clickPalette) {
PaletteSelectorDialog(
initKey = themeIndex,
onDismiss ={
clickPalette = false
},
onSelectItem = {
viewModel.themeIndex.value = it
viewModel.selectTheme.value = themeColors[it]
}
)
}
if (clickExitApp) {
SampleAlertDialog(
title = "提示",
content = "退出后,将无法查看我的文章、消息、收藏、积分、浏览记录等功能,确定退出登录吗?",
onConfirmClick = {
viewModel.logout()
},
onDismiss = {
clickExitApp = false
}
)
}
if (isAboutMe) {
InfoDialog(
content = arrayOf(
"作者: SuperMAN",
"email: ganzhuangman@gmail.com",
"版本号: v1.0",
"项目源码: https://github.com/manqianzhuang/HamApp.git",
"版权声明: 本app仅用于学习用处,不得抄袭用于商业行为",
),
onDismiss = {
isAboutMe = false
}
)
}
if (isClearCache) {
SampleAlertDialog(
title = "提示",
content = "清除缓存后,缓存文件夹中的照片或文件可能丢失,确定清除吗?",
onConfirmClick = {
coroutineScope.launch {
val cacheResult = withContext(Dispatchers.IO) {
CacheDataManager.clearAllCache(context)
val size = CacheDataManager.getTotalCacheSize(context)
arrayOf("缓存已清理", size)
}
withContext(Dispatchers.Main) {
isClearCache = false
tipsMessage = cacheResult[0]
cacheSize = cacheResult[1]
}
}
},
onDismiss = {
isClearCache = false
}
)
}
}
@@ -0,0 +1,65 @@
package com.mm.hamcompose.ui.page.main.profile.settings
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.Color
import com.blankj.utilcode.util.SPUtils
import com.mm.hamcompose.data.bean.UserInfo
import com.mm.hamcompose.data.db.user.UserInfoDatabase
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.data.store.DataStoreUtils
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.theme.THEME_COLOR_KEY
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val repo: HttpRepository,
private val db: UserInfoDatabase
): BaseViewModel<UserInfo>() {
val cacheSize = mutableStateOf(0)
var logout = mutableStateOf(false)
var themeIndex = mutableStateOf(SPUtils.getInstance().getInt(THEME_COLOR_KEY, 0))
var selectTheme = mutableStateOf<Color?>(null)
override fun start() {
initThat { }
}
fun logout() {
async {
repo.logout().collectLatest {
when(it) {
is HttpResult.Success -> { }
is HttpResult.Error -> {
//退出登录的情况下,不走success判断分支
val nullNotice = "the result of remote's request is null"
if (it.exception.message==nullNotice) {
clearUserInfo()
}
}
}
}
}
}
private fun clearUserInfo() {
async {
withContext(Dispatchers.IO) {
db.userInfoDao().deleteAllUserInfo()
DataStoreUtils.clear()
delay(10)
logout.value = true
}
}
}
}
@@ -0,0 +1,226 @@
package com.mm.hamcompose.ui.page.main.profile.sharer
import android.annotation.SuppressLint
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.ScaffoldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.bean.MY_USER_ID
import com.mm.hamcompose.data.bean.PointsBean
import com.mm.hamcompose.data.bean.WebData
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.theme.white1
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.*
import com.mm.hamcompose.util.RegexUtils
@SuppressLint("CoroutineCreationDuringComposition")
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SharerPage(
userId: Int?,
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: SharerViewModel = hiltViewModel()
) {
if (userId != null) {
viewModel.setupUserId(userId)
}
viewModel.start()
val errorMessage by remember { viewModel.errorMessage }
val loading by remember { viewModel.loading }
val loadingMore by remember { viewModel.isLoadingMore }
val points by remember { viewModel.points }
val articles = remember { viewModel.articles }
val currentPosition by remember { viewModel.currentListIndex }
var editable by remember { mutableStateOf(false) }
val listState = rememberLazyListState(currentPosition)
Column(
modifier = Modifier.fillMaxSize()
) {
//头部栏
HamToolBar(
title = points?.username ?: "",
onBack = { navCtrl.back() },
onRightClick = {
editable = !editable
},
imageVector = if (userId == MY_USER_ID && !articles.isNullOrEmpty()) Icons.Default.Edit else null
)
if (loading) {
Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(
color = HamTheme.colors.themeUi,
modifier = Modifier.align(Alignment.Center)
)
}
} else {
//滚动区域
LazyColumn(
modifier = Modifier.weight(1f),
state = listState
) {
item { UserInfo(points = points) }
stickyHeader { ListTitle(title = "文章列表") }
if (!articles.isNullOrEmpty()) {
itemsIndexed(articles) { index, article ->
Row(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp)
) {
TextContent(
text = "${index + 1}. ${article.title}",
maxLines = 2,
modifier = Modifier
.weight(1f)
.clickable {
val webData = WebData(article.title, article.link!!)
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, webData)
}
)
if (editable) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = null,
tint = HamTheme.colors.textSecondary,
modifier = Modifier.clickable {
viewModel.deleteMyArticle(article.id)
}
)
}
}
}
if (loadingMore) {
item {
LinearProgressIndicator(
modifier = Modifier
.height(5.dp)
.fillMaxWidth(),
color = HamTheme.colors.themeUi
)
}
}
if (listState.firstVisibleItemIndex >= (articles.size-25)) {
viewModel.nextPage()
}
} else {
item {
EmptyView(tips = errorMessage)
}
}
}
}
}
}
@Composable
private fun UserInfo(points: PointsBean?) {
Row(
modifier = Modifier
.fillMaxSize()
.height(56.dp)
.background(color = HamTheme.colors.themeUi)
) {
if (points != null) {
Column(
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1f)
) {
MiniTitle(
text = "等级",
modifier = Modifier.align(Alignment.CenterHorizontally),
color = white1
)
TextContent(
text = "${points.level ?: 0}",
modifier = Modifier
.padding(top = 5.dp)
.align(Alignment.CenterHorizontally),
color = white1
)
}
Column(
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1f)
) {
MiniTitle(
text = "积分",
modifier = Modifier.align(Alignment.CenterHorizontally),
color = white1
)
TextContent(
text = "${points.coinCount ?: 0}",
modifier = Modifier
.padding(top = 5.dp)
.align(Alignment.CenterHorizontally),
color = white1
)
}
Column(
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1f)
) {
MiniTitle(
text = "排行",
modifier = Modifier.align(Alignment.CenterHorizontally),
color = white1
)
TextContent(
text = "${points.rank ?: 0}",
modifier = Modifier
.padding(top = 5.dp)
.align(Alignment.CenterHorizontally),
color = white1
)
}
}
}
}
@Composable
private fun ArticleItem(
index: Int,
article: Article,
onItemClick: () -> Unit
) {
Column(
Modifier
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
.clickable { onItemClick.invoke() }
) {
TextContent(text = "${index + 1}. ${article.title}")
Row(
modifier = Modifier.align(Alignment.End)
) {
TimerIcon(modifier = Modifier.padding(start = 10.dp))
MiniTitle(text = "${RegexUtils().timestamp(article.niceDate)}")
}
}
}
@@ -0,0 +1,101 @@
package com.mm.hamcompose.ui.page.main.profile.sharer
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import com.mm.hamcompose.data.bean.Article
import com.mm.hamcompose.data.bean.MY_USER_ID
import com.mm.hamcompose.data.bean.PointsBean
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest
import javax.inject.Inject
@HiltViewModel
class SharerViewModel @Inject constructor(private val repo: HttpRepository): BaseViewModel<Article>() {
private var page = mutableStateOf(1)
var points = mutableStateOf<PointsBean?>(null)
var articles = mutableStateListOf<Article>()
private val userId = mutableStateOf(-1)
var isLoadingMore = mutableStateOf(false)
var hasMore = mutableStateOf(false)
var errorMessage = mutableStateOf("")
fun setupUserId(id: Int) {
this.userId.value = id
}
override fun start() {
initThat {
startLoading()
getShareData()
}
}
fun nextPage() {
if (hasMore.value) {
page.value += 1
getShareData()
}
}
private fun getShareData() {
async {
isLoadingMore.value = true
val call = if (userId.value == MY_USER_ID) {
repo.getMyShareArticles(page.value)
} else {
repo.getAuthorShareArticles(userId.value, page.value)
}
call.collectLatest { response ->
when (response) {
is HttpResult.Success -> {
points.value = response.result.coinInfo
response.result.shareArticles.datas.run {
if (!isNullOrEmpty()) {
articles.addAll(this)
}
errorMessage.value = if (isNullOrEmpty()) "啥都没有~" else ""
hasMore.value = !response.result.shareArticles.over && size >= 0
}
}
is HttpResult.Error -> {
println(response.exception.message)
errorMessage.value = response.exception.message ?: "未知异常"
}
}
resetStatus()
}
}
}
private fun resetStatus() {
stopLoading()
isLoadingMore.value = false
}
fun deleteMyArticle(id: Int) {
async {
repo.deleteMyShareArticle(id).collectLatest { response ->
when (response) {
is HttpResult.Success -> {
}
is HttpResult.Error -> {
println(response.exception.message)
val error = response.exception.message
if (error == "the result of remote's request is null") {
val deleteItem = articles.find { it.id == id }
articles.remove(deleteItem)
}
}
}
}
}
}
}
@@ -0,0 +1,159 @@
package com.mm.hamcompose.ui.page.main.profile.user
import android.annotation.SuppressLint
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Icon
import androidx.compose.material.ScaffoldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.theme.ToolBarHeight
import com.mm.hamcompose.ui.route.RouteName
import com.mm.hamcompose.ui.route.RouteUtils
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.*
import kotlinx.coroutines.launch
@SuppressLint("CoroutineCreationDuringComposition")
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun LoginPage(
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: UserViewModel = hiltViewModel()
) {
var userAccount by remember { mutableStateOf("") }
var userPassword by remember { mutableStateOf("") }
val isLogin by remember { viewModel.isLogin }
val errorMessage by remember { viewModel.errorMessage }
val keyboardController = LocalSoftwareKeyboardController.current
val coroutineState = rememberCoroutineScope()
/**
* 重点保存Bundle状态以防从下一页返回时数据丢失
*/
val accountSaver = run {
val accountKey = "userAccount"
val passwordKey = "userPassword"
mapSaver (
save = {
mapOf(accountKey to userAccount, passwordKey to userPassword)
},
restore = {
userAccount = it[accountKey] as String
userPassword = it[passwordKey] as String
})
}
rememberSaveable(userAccount, userPassword, saver = accountSaver) { }
if (isLogin) {
navCtrl.popBackStack()
viewModel.isLogin.value = false
}
//SnackBar弹窗显示信息
if (errorMessage!=null) {
popupSnackBar(coroutineState, scaffoldState, label = SNACK_ERROR, errorMessage!!)
viewModel.errorMessage.value = null
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(HamTheme.colors.themeUi)
.pointerInput(Unit) {
detectTapGestures(
onPress = {
keyboardController?.hide()
}
)
},
) {
item {
Box(
modifier = Modifier
.padding(bottom = 80.dp)
.fillMaxWidth()
.height(ToolBarHeight)
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
tint = HamTheme.colors.mainColor,
modifier = Modifier
.padding(start = 20.dp)
.align(Alignment.CenterStart)
.clickable { navCtrl.back() }
)
TextContent(
text = "用户注册",
modifier = Modifier
.padding(end = 20.dp)
.align(Alignment.CenterEnd)
.clickable {
RouteUtils.navTo(navCtrl, RouteName.REGISTER)
},
color = HamTheme.colors.mainColor
)
}
}
item {
Box(Modifier.fillMaxWidth()) {
LargeTitle(
title = "WanAndroid",
modifier = Modifier
.padding(bottom = 50.dp)
.align(Alignment.Center),
color = HamTheme.colors.mainColor
)
}
}
item {
LoginEditView(
text = userAccount,
labelText = "账号",
hintText = "请输入用户名",
onValueChanged = { userAccount = it },
onDeleteClick = { userAccount = "" }
)
}
item {
LoginEditView(
text = userPassword,
labelText = "密码",
hintText = "请输入密码",
onValueChanged = { userPassword = it },
onDeleteClick = { userPassword = "" },
modifier = Modifier.padding(top = 20.dp, bottom = 20.dp),
isPassword = true
)
}
item {
HamButton(
text = "登录",
modifier = Modifier.padding(horizontal = 20.dp)
) {
keyboardController?.hide()
viewModel.errorMessage.value = null
viewModel.login(userAccount.trim(), userPassword.trim())
}
}
}
}
@@ -0,0 +1,155 @@
package com.mm.hamcompose.ui.page.main.profile.user
import android.annotation.SuppressLint
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Icon
import androidx.compose.material.ScaffoldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.mm.hamcompose.theme.HamTheme
import com.mm.hamcompose.theme.ToolBarHeight
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.*
import kotlinx.coroutines.launch
@SuppressLint("CoroutineCreationDuringComposition")
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun RegisterPage(
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: UserViewModel = hiltViewModel()
) {
val registerSuccess by remember { viewModel.isRegister }
var userAccount by remember { mutableStateOf("") }
var userPassword by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") }
var inviteCode by remember { mutableStateOf("") }
val keyboardController = LocalSoftwareKeyboardController.current
val errorMessage by remember { viewModel.errorMessage }
val coroutineState = rememberCoroutineScope()
if (registerSuccess) {
navCtrl.back()
}
//SnackBar弹窗显示信息
if (errorMessage!=null) {
popupSnackBar(coroutineState, scaffoldState, label = SNACK_ERROR, errorMessage!!)
viewModel.errorMessage.value = null
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(HamTheme.colors.themeUi)
.pointerInput(Unit) {
detectTapGestures(
onPress = {
keyboardController?.hide()
}
)
},
) {
item {
Box(Modifier
.padding(bottom = 80.dp)
.fillMaxWidth()
.height(ToolBarHeight)
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
tint = HamTheme.colors.mainColor,
modifier = Modifier
.padding(start = 20.dp)
.align(Alignment.CenterStart)
.clickable { navCtrl.back() }
)
MediumTitle(
title = "用户注册",
modifier = Modifier.align(Alignment.Center),
//color = HamTheme.colors.mainColor
)
}
}
item {
LoginEditView(
text = userAccount,
labelText = "账号",
hintText = "请输入用户名",
onValueChanged = { userAccount = it },
onDeleteClick = { userAccount = "" }
)
}
item {
LoginEditView(
text = userPassword,
labelText = "密码",
hintText = "请输入密码",
onValueChanged = { userPassword = it },
onDeleteClick = { userPassword = "" },
modifier = Modifier.padding(top = 20.dp, bottom = 20.dp),
isPassword = true
)
}
item {
LoginEditView(
text = confirmPassword,
labelText = "再次输入密码",
hintText = "请再次输入密码",
onValueChanged = { confirmPassword = it },
onDeleteClick = { confirmPassword = "" },
modifier = Modifier.padding(bottom = 20.dp),
isPassword = true
)
}
item {
LoginEditView(
text = inviteCode,
labelText = "输入邀请码",
hintText = "请输入邀请码",
onValueChanged = { inviteCode = it },
onDeleteClick = { inviteCode = "" },
modifier = Modifier.padding(bottom = 20.dp),
)
}
item {
HamButton(
text = "去注册",
modifier = Modifier.padding(horizontal = 20.dp)
) {
keyboardController?.hide()
val isVerifyAllRight = checkInputsAreNotNull(userAccount, userPassword, confirmPassword, inviteCode)
if (isVerifyAllRight) {
viewModel.register(userAccount.trim(), userPassword.trim(), confirmPassword.trim())
}
}
}
}
}
private fun checkInputsAreNotNull(
account: String,
password: String,
repassword: String,
inviteCode: String
): Boolean {
return account.isNotEmpty() && password.isNotEmpty() && repassword.isNotEmpty() && inviteCode.isNotEmpty()
}
@@ -0,0 +1,74 @@
package com.mm.hamcompose.ui.page.main.profile.user
import androidx.compose.runtime.mutableStateOf
import com.blankj.utilcode.util.LogUtils
import com.mm.hamcompose.data.bean.UserInfo
import com.mm.hamcompose.data.http.HttpResult
import com.mm.hamcompose.repository.HttpRepository
import com.mm.hamcompose.data.db.user.UserInfoDatabase
import com.mm.hamcompose.ui.page.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class UserViewModel @Inject constructor(
private val repo: HttpRepository,
private val userDb: UserInfoDatabase
) : BaseViewModel<UserInfo>() {
var errorMessage = mutableStateOf<String?>(null)
var isRegister = mutableStateOf(false)
var isLogin = mutableStateOf(false)
override fun start() {}
fun register(account: String, password: String, repassword: String) {
async {
repo.register(account, password, repassword)
.collectLatest { response ->
when (response) {
is HttpResult.Success -> {
isRegister.value = true
}
is HttpResult.Error -> {
errorMessage.value = response.exception.message
//ToastUtils.showShort(errorMessage.value)
}
}
}
}
}
fun login(account: String, password: String) {
async {
repo.login(account, password)
.collectLatest { response ->
when (response) {
is HttpResult.Success -> {
saveUserInfo(response.result)
isLogin.value = true
}
is HttpResult.Error -> {
errorMessage.value = response.exception.message
}
}
}
}
}
override fun onCleared() {
super.onCleared()
LogUtils.e("invoke onCleared of ViewModel")
}
private fun saveUserInfo(userInfo: UserInfo) {
async {
withContext(Dispatchers.IO) {
userDb.userInfoDao().insertUserInfo(userInfo)
}
}
}
}
@@ -0,0 +1,151 @@
package com.mm.hamcompose.ui.page.splash
import android.annotation.SuppressLint
import android.os.CountDownTimer
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import com.blankj.utilcode.util.LogUtils
import com.mm.hamcompose.R
import com.mm.hamcompose.theme.splashText
import com.mm.hamcompose.theme.white1
import com.mm.hamcompose.ui.widget.TextContent
import kotlin.random.Random
@OptIn(ExperimentalAnimationApi::class)
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun SplashPage(onNextPage: () -> Unit) {
val imageRes = listOf(
R.mipmap.splash_image01,
R.mipmap.splash_image02,
R.mipmap.splash_image03,
R.mipmap.splash_image04,
R.mipmap.splash_image05
)
val bgImage = imageRes[Random.nextInt(imageRes.size)]
var interval by remember { mutableStateOf(5) }
val timer = SplashTimer(
onTickMillis = { if (interval >= 0) { interval -= 1 } },
onFinished = { onNextPage() }
).start()
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
val (tips, version, play, android) = createRefs()
Image(
painter = painterResource(id = bgImage),
contentDescription = "背景图",
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
contentScale = ContentScale.FillBounds
)
SplashIntervalText(
title = interval.toString(),
modifier = Modifier
.padding(top = 60.dp, end = 20.dp)
.constrainAs(tips) {
top.linkTo(parent.top)
end.linkTo(parent.end)
},
doClick = {
timer.cancel()
onNextPage()
})
TextContent(
text = "Version: 1.0.0",
color = white1,
modifier = Modifier
.padding(bottom = 80.dp)
.constrainAs(version) {
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
Text(
text = "Wan",
fontSize = 36.sp,
color = white1,
modifier = Modifier
.padding(bottom = 80.dp, end = 100.dp)
.constrainAs(play) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
},
)
Text(
text = "Android",
modifier = Modifier
.padding(top = 50.dp, start = 100.dp)
.constrainAs(android) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
},
fontSize = 36.sp,
color = white1
)
}
}
class SplashTimer(
val onTickMillis: () -> Unit,
val onFinished: () -> Unit
) : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {
//LogUtils.i("onTick = $millisUntilFinished")
onTickMillis()
}
override fun onFinish() {
onFinished()
}
}
@Composable
fun SplashIntervalText(title: String, modifier: Modifier, doClick: () -> Unit) {
Box(
modifier = modifier
.width(60.dp)
.height(30.dp)
.background(splashText, RoundedCornerShape(15.dp))
.clickable {
doClick()
}
) {
TextContent(
text = if (title == "-1") "进入" else "${title}s",
color = white1,
modifier = Modifier.align(Alignment.Center)
)
}
}
@@ -0,0 +1,77 @@
package com.mm.hamcompose.ui.page.webview
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.view.ViewGroup
import android.webkit.WebView
import android.widget.FrameLayout
import android.widget.ProgressBar
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.navigation.NavHostController
import com.blankj.utilcode.util.SizeUtils
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.mm.hamcompose.R
import com.mm.hamcompose.data.bean.WebData
import com.mm.hamcompose.theme.ToolBarHeight
import com.mm.hamcompose.ui.route.RouteUtils.back
import com.mm.hamcompose.ui.widget.HamToolBar
@SuppressLint("UseCompatLoadingForDrawables")
@Composable
fun WebViewPage(
webData: WebData,
navCtrl: NavHostController
) {
var ctrl: WebViewCtrl? by remember { mutableStateOf(null) }
Box {
var isRefreshing: Boolean by remember { mutableStateOf(false) }
val refreshState = rememberSwipeRefreshState(isRefreshing)
AndroidView(
modifier = Modifier
.padding(top = ToolBarHeight)
.fillMaxSize(),
factory = { context ->
FrameLayout(context).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT
)
val progressView = ProgressBar(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
SizeUtils.dp2px(2f)
)
progressDrawable =
context.resources.getDrawable(R.drawable.horizontal_progressbar)
indeterminateTintList =
ColorStateList.valueOf(context.resources.getColor(R.color.teal_200))
}
val webView = WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
)
}
addView(webView)
addView(progressView)
ctrl = WebViewCtrl(this, webData.url, onWebCall = { isFinish ->
isRefreshing = !isFinish
})
ctrl?.initSettings()
}
},
update = {
}
)
HamToolBar(title = webData.title ?: "标题", onBack = {
ctrl?.onDestroy()
navCtrl.back()
})
}
}
@@ -0,0 +1,112 @@
package com.mm.hamcompose.ui.page.webview
import android.graphics.Bitmap
import android.net.http.SslError
import android.os.Build
import android.view.View
import android.webkit.*
import android.widget.FrameLayout
import android.widget.ProgressBar
import androidx.annotation.RequiresApi
class WebViewCtrl(
private val mView: FrameLayout,
private var linkUrl: String,
private val onWebCall: (isFinish: Boolean) -> Unit
) {
private val webView by lazy { mView.getChildAt(0) as WebView }
private val progressBar by lazy { mView.getChildAt(1) as ProgressBar }
fun initSettings() {
onWebCall(false)
setWebSettings()
setupWebClient()
}
fun onDestroy() {
mView.removeAllViews()
webView.destroy()
}
private fun setWebSettings() {
val webSettings = webView.settings
//如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
webSettings.javaScriptEnabled = false
//设置自适应屏幕,两者合用
webSettings.useWideViewPort = true //将图片调整到适合webview的大小
webSettings.loadWithOverviewMode = true // 缩放至屏幕的大小
//缩放操作
webSettings.setSupportZoom(true) //支持缩放,默认为true。是下面那个的前提。
webSettings.builtInZoomControls = true //设置内置的缩放控件。若为false,则该WebView不可缩放
webSettings.displayZoomControls = false //隐藏原生的缩放控件
//其他细节操作
webSettings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK //关闭webview中缓存
webSettings.allowFileAccess = true //设置可以访问文件
webSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口
webSettings.loadsImagesAutomatically = true //支持自动加载图片
webSettings.defaultTextEncodingName = "UTF-8"//设置编码格式
}
private fun setupWebClient() {
webView.webViewClient = NewWebViewClient()
webView.webChromeClient = ProgressWebViewChromeClient()
refresh()
}
fun refresh() {
webView.loadUrl(linkUrl)
}
inner class ProgressWebViewChromeClient : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
super.onProgressChanged(view, newProgress)
progressBar.progress = newProgress
}
override fun onReceivedTitle(view: WebView?, title: String?) {
super.onReceivedTitle(view, title)
}
}
inner class NewWebViewClient : WebViewClient() {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
linkUrl = request?.url.toString()
return super.shouldOverrideUrlLoading(view, request)
}
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
linkUrl = url?:"NullUrlString"
return super.shouldOverrideUrlLoading(view, url)
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
progressBar.visibility = View.VISIBLE
super.onPageStarted(view, url, favicon)
}
override fun onPageFinished(view: WebView?, url: String?) {
progressBar.visibility = View.GONE
onWebCall(true)
super.onPageFinished(view, url)
}
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?
) {
handler?.proceed()
}
}
}

Some files were not shown because too many files have changed in this diff Show More