a
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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
@@ -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
|
||||
+24
@@ -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"
|
||||
}
|
||||
+21
@@ -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()
|
||||
|
||||
}
|
||||
+11
@@ -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
|
||||
}
|
||||
+11
@@ -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
|
||||
}
|
||||
+24
@@ -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()
|
||||
|
||||
}
|
||||
+28
@@ -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()
|
||||
|
||||
|
||||
|
||||
}
|
||||
+14
@@ -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
|
||||
|
||||
|
||||
}
|
||||
+57
@@ -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()
|
||||
}
|
||||
+134
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+22
@@ -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())
|
||||
}
|
||||
}
|
||||
+42
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -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
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
+304
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -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()
|
||||
+7
@@ -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()
|
||||
+77
@@ -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
|
||||
|
||||
}
|
||||
+181
@@ -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() }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+50
@@ -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 ?: "未知异常"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
+64
@@ -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() { }
|
||||
|
||||
}
|
||||
+40
@@ -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 ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+246
@@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+77
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
+139
@@ -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 */
|
||||
}
|
||||
+58
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
+119
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+36
@@ -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() }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+125
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+147
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
@@ -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 -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+193
@@ -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
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+58
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+109
@@ -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 = "" },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+54
@@ -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 = "分享成功"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+220
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+67
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
+114
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+38
@@ -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() }
|
||||
}
|
||||
}
|
||||
+184
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+135
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+151
@@ -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 = "" },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+88
@@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
+170
@@ -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
|
||||
// )
|
||||
// }
|
||||
//}
|
||||
+50
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
+136
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+114
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
+274
@@ -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
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+85
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
+287
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
+116
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+102
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+150
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -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)
|
||||
|
||||
}
|
||||
+504
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+124
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
+82
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+40
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+81
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -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)
|
||||
}
|
||||
+75
@@ -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)
|
||||
|
||||
}
|
||||
+286
@@ -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)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+172
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+65
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+226
@@ -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)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+159
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+155
@@ -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()
|
||||
}
|
||||
+74
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+151
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
+77
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
+112
@@ -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
Reference in New Issue
Block a user