a
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,94 @@
|
||||
fun properties(key: String) = project.findProperty(key).toString()
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("com.google.dagger.hilt.android")
|
||||
kotlin("kapt")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.postliu.wanandroid"
|
||||
compileSdk = 33
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.postliu.wanandroid"
|
||||
minSdk = 24
|
||||
targetSdk = 33
|
||||
versionCode = 100
|
||||
versionName = "1.0.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
|
||||
buildConfigField("String", "BASE_IP", properties("BASE_URL"))
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = 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"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.3.2"
|
||||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes.add("/META-INF/{AL2.0,LGPL2.1}")
|
||||
}
|
||||
}
|
||||
hilt {
|
||||
enableExperimentalClasspathAggregation = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val composeVersion = "1.3.0"
|
||||
val accompanistVersion = "0.27.0"
|
||||
implementation("androidx.paging:paging-compose:1.0.0-alpha17")
|
||||
implementation("com.github.zhujiang521:Banner:2.4.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha03")
|
||||
implementation("com.github.ihsanbal:LoggingInterceptor:3.1.0") {
|
||||
exclude(group = "org.json", module = "json")
|
||||
}
|
||||
implementation("com.google.code.gson:gson:2.10")
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||
implementation("io.coil-kt:coil-compose:2.2.2")
|
||||
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
|
||||
implementation("androidx.navigation:navigation-compose:2.5.3")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
|
||||
implementation("com.google.dagger:hilt-android:2.44")
|
||||
kapt("com.google.dagger:hilt-compiler:2.44")
|
||||
implementation("com.google.accompanist:accompanist-swiperefresh:$accompanistVersion")
|
||||
implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
|
||||
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
|
||||
implementation("androidx.core:core-ktx:1.8.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1")
|
||||
implementation("androidx.activity:activity-compose:1.5.1")
|
||||
implementation("androidx.compose.ui:ui:$composeVersion")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion")
|
||||
implementation("androidx.compose.material:material:1.3.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$composeVersion")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest:$composeVersion")
|
||||
}
|
||||
@@ -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.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.postliu.wanandroid
|
||||
|
||||
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.postliu.wanandroid", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".WanAndroidApp"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.WanAndroid"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.postliu.wanandroid
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.postliu.wanandroid.ui.main.MainPage
|
||||
import com.postliu.wanandroid.ui.main.MainViewModel
|
||||
import com.postliu.wanandroid.ui.theme.WanAndroidTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
setContent {
|
||||
WanAndroidTheme {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val useDarkIcons = !isSystemInDarkTheme()
|
||||
val statusBarColor = MaterialTheme.colors.primary
|
||||
DisposableEffect(key1 = systemUiController, key2 = useDarkIcons) {
|
||||
systemUiController.setSystemBarsColor(color = statusBarColor, useDarkIcons)
|
||||
onDispose { }
|
||||
}
|
||||
MainPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.postliu.wanandroid
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class WanAndroidApp : Application() {
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.postliu.wanandroid.common
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
||||
abstract class BasePagingSource<Value : Any> : PagingSource<Int, Value>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, Value>): 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, Value> {
|
||||
return loadPaging(params)
|
||||
}
|
||||
|
||||
abstract suspend fun loadPaging(loadParams: LoadParams<Int>): LoadResult<Int, Value>
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个默认的[PagingConfig]
|
||||
*/
|
||||
val defaultPagingConfig = PagingConfig(pageSize = 10, initialLoadSize = 10, prefetchDistance = 1)
|
||||
|
||||
/**
|
||||
* 封装[PagingSource],简化创建流程
|
||||
*
|
||||
* @param T 数据类型
|
||||
* @param block 具体的请求结果块
|
||||
*/
|
||||
inline fun <reified T : Any> intKeyPagingSource(
|
||||
crossinline block: suspend (page: Int) -> List<T>
|
||||
) = object : BasePagingSource<T>() {
|
||||
|
||||
override suspend fun loadPaging(loadParams: LoadParams<Int>): LoadResult<Int, T> {
|
||||
return kotlin.runCatching {
|
||||
val page = loadParams.key ?: 1
|
||||
val dataList = block.invoke(page)
|
||||
LoadResult.Page(
|
||||
data = dataList,
|
||||
prevKey = null,
|
||||
nextKey = if (dataList.size < 10) null else page + 1
|
||||
)
|
||||
}.getOrElse { LoadResult.Error(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 封装[Pager],简化创建流程
|
||||
*
|
||||
* @param T 数据源类型
|
||||
* @param block 数据请求块
|
||||
*/
|
||||
inline fun <reified T : Any> launchPagingFlow(
|
||||
crossinline block: suspend (page: Int) -> List<T>
|
||||
) = Pager(
|
||||
config = defaultPagingConfig,
|
||||
pagingSourceFactory = {
|
||||
intKeyPagingSource {
|
||||
block.invoke(it)
|
||||
}
|
||||
}
|
||||
).flow.flowOn(Dispatchers.IO)
|
||||
|
||||
inline fun <reified T : Any> intKeyZeroPagingSource(
|
||||
crossinline block: suspend (page: Int) -> List<T>
|
||||
) = object : BasePagingSource<T>() {
|
||||
|
||||
override suspend fun loadPaging(loadParams: LoadParams<Int>): LoadResult<Int, T> {
|
||||
return kotlin.runCatching {
|
||||
val page = loadParams.key ?: 0
|
||||
val dataList = block.invoke(page)
|
||||
LoadResult.Page(
|
||||
data = dataList,
|
||||
prevKey = null,
|
||||
nextKey = if (dataList.size < 10) null else page + 1
|
||||
)
|
||||
}.getOrElse { LoadResult.Error(it) }
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> launchPagingFromZeroFlow(
|
||||
crossinline block: suspend (page: Int) -> List<T>
|
||||
) = Pager(
|
||||
config = defaultPagingConfig,
|
||||
pagingSourceFactory = {
|
||||
intKeyZeroPagingSource {
|
||||
block.invoke(it)
|
||||
}
|
||||
}
|
||||
).flow.flowOn(Dispatchers.IO)
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.postliu.wanandroid.common
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
|
||||
fun Context.toast(msg: Any?) {
|
||||
msg?.let { Toast.makeText(this, it.toString(), Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.postliu.wanandroid.common
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
||||
object GsonUtils {
|
||||
private val gson by lazy { Gson() }
|
||||
|
||||
fun toJson(json: Any): String = gson.toJson(json)
|
||||
|
||||
fun <T> fromJson(json: String, typeToken: TypeToken<T>): T = gson.fromJson(json, typeToken)
|
||||
|
||||
fun <T> fromJson(json: String, clazz: Class<T>): T = gson.fromJson(json, clazz)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.postliu.wanandroid.common
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import okio.IOException
|
||||
|
||||
class TokenException(val code: Int, override val message: String) : IOException(message)
|
||||
|
||||
class QuestException(val code: Int, override val message: String) : IOException(message)
|
||||
|
||||
@Keep
|
||||
data class DataResult<out T>(
|
||||
val errorCode: Int,
|
||||
val errorMsg: String,
|
||||
@SerializedName("data")
|
||||
val `data`: T
|
||||
) {
|
||||
val result = when (errorCode) {
|
||||
0 -> data
|
||||
-1001 -> throw TokenException(errorCode, errorMsg)
|
||||
else -> throw QuestException(errorCode, errorMsg)
|
||||
}
|
||||
|
||||
val success get() = errorCode == 0
|
||||
}
|
||||
|
||||
sealed interface UIResult<out T> {
|
||||
object Loading : UIResult<Nothing>
|
||||
data class Throwable(val throwable: kotlin.Throwable) : UIResult<Nothing>
|
||||
data class Failed(val code: Int, val message: String) : UIResult<Nothing>
|
||||
data class Success<T>(val data: T) : UIResult<T>
|
||||
|
||||
companion object {
|
||||
|
||||
inline fun <T> UIResult<T>.doLoading(block: () -> Unit) {
|
||||
if (this is Loading) {
|
||||
block.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> UIResult<T>.doThrowable(block: (kotlin.Throwable) -> Unit) {
|
||||
if (this is Throwable) {
|
||||
block.invoke(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> UIResult<T>.doFailed(block: (String) -> Unit) {
|
||||
if (this is Failed) {
|
||||
block.invoke(message)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> UIResult<T>.doSuccess(block: (T) -> Unit) {
|
||||
if (this is Success) {
|
||||
block.invoke(data)
|
||||
}
|
||||
}
|
||||
|
||||
val <T> UIResult<T>.successData get() = if (this is Success) data else null
|
||||
|
||||
val <T> UIResult<T>.failedMsg get() = if (this is Failed) message else ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.postliu.wanandroid.common
|
||||
|
||||
object Routes {
|
||||
|
||||
const val Project = "/project"
|
||||
const val System = "/system"
|
||||
const val Official = "/official"
|
||||
const val Square = "/square"
|
||||
|
||||
const val Register = "/register"
|
||||
|
||||
const val Login = "/login"
|
||||
|
||||
const val Home = "/home"
|
||||
|
||||
const val Main = "/main"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.postliu.wanandroid.di
|
||||
|
||||
import com.postliu.wanandroid.model.ApiService
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import retrofit2.Retrofit
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ApiServiceModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesApiService(retrofit: Retrofit): ApiService {
|
||||
return retrofit.create(ApiService::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.postliu.wanandroid.di
|
||||
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.ToNumberPolicy
|
||||
import com.ihsanbal.logging.Level
|
||||
import com.ihsanbal.logging.LoggingInterceptor
|
||||
import com.postliu.wanandroid.BuildConfig
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object RetrofitModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providerOkHttpClient(): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(
|
||||
LoggingInterceptor.Builder()
|
||||
.setLevel(Level.BODY)
|
||||
.build()
|
||||
).build()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providerRetrofit(okHttpClient: OkHttpClient): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BuildConfig.BASE_IP)
|
||||
.addConverterFactory(
|
||||
GsonConverterFactory.create(
|
||||
GsonBuilder().setObjectToNumberStrategy(
|
||||
ToNumberPolicy.LAZILY_PARSED_NUMBER
|
||||
).create()
|
||||
)
|
||||
).client(okHttpClient).build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.postliu.wanandroid.model
|
||||
|
||||
import com.postliu.wanandroid.common.DataResult
|
||||
import com.postliu.wanandroid.model.entity.ArticleEntity
|
||||
import com.postliu.wanandroid.model.entity.HomeArticleEntity
|
||||
import com.postliu.wanandroid.model.entity.LoginUserEntity
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface ApiService {
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @param userName 用户名
|
||||
* @param password 登录密码
|
||||
* @return
|
||||
*/
|
||||
@FormUrlEncoded
|
||||
@POST("/user/login")
|
||||
suspend fun login(
|
||||
@Field("username") userName: String,
|
||||
@Field("password") password: String,
|
||||
): DataResult<LoginUserEntity>
|
||||
|
||||
/**
|
||||
* 注册账号
|
||||
*
|
||||
* @param userName 用户名
|
||||
* @param password 密码
|
||||
* @param rePassword 确认密码
|
||||
* @return
|
||||
*/
|
||||
@FormUrlEncoded
|
||||
@POST("/user/register")
|
||||
suspend fun register(
|
||||
@Field("username") userName: String,
|
||||
@Field("password") password: String,
|
||||
@Field("repassword") rePassword: String,
|
||||
): DataResult<LoginUserEntity>
|
||||
|
||||
/**
|
||||
* 置顶文章
|
||||
*
|
||||
*/
|
||||
@GET("/article/top/json")
|
||||
suspend fun stickyPostsArticle(): DataResult<List<ArticleEntity>>
|
||||
|
||||
@GET("/article/list/{page}/json")
|
||||
suspend fun homeArticle(
|
||||
@Path("page") page: Int = 0,
|
||||
@Query("page_size") pageSize: Int = 10
|
||||
): DataResult<HomeArticleEntity>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.postliu.wanandroid.model.entity
|
||||
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* 文章信息
|
||||
*
|
||||
* @property adminAdd
|
||||
* @property apkLink
|
||||
* @property audit
|
||||
* @property author
|
||||
* @property canEdit
|
||||
* @property chapterId
|
||||
* @property chapterName
|
||||
* @property collect 是否已收藏
|
||||
* @property courseId
|
||||
* @property desc
|
||||
* @property descMd
|
||||
* @property envelopePic
|
||||
* @property fresh 是否是最新文章
|
||||
* @property host
|
||||
* @property id
|
||||
* @property isAdminAdd
|
||||
* @property link
|
||||
* @property niceDate 文章时间
|
||||
* @property niceShareDate 分享文章时间
|
||||
* @property origin
|
||||
* @property prefix
|
||||
* @property projectLink
|
||||
* @property publishTime
|
||||
* @property realSuperChapterId
|
||||
* @property selfVisible
|
||||
* @property shareDate
|
||||
* @property shareUser
|
||||
* @property superChapterId
|
||||
* @property superChapterName
|
||||
* @property tags
|
||||
* @property title
|
||||
* @property type
|
||||
* @property userId
|
||||
* @property visible
|
||||
* @property zan
|
||||
* @constructor Create empty Article entity
|
||||
*/
|
||||
@Keep
|
||||
data class ArticleEntity(
|
||||
@SerializedName("adminAdd")
|
||||
val adminAdd: Boolean,
|
||||
@SerializedName("apkLink")
|
||||
val apkLink: String,
|
||||
@SerializedName("audit")
|
||||
val audit: Int,
|
||||
@SerializedName("author")
|
||||
val author: String,
|
||||
@SerializedName("canEdit")
|
||||
val canEdit: Boolean,
|
||||
@SerializedName("chapterId")
|
||||
val chapterId: Int,
|
||||
@SerializedName("chapterName")
|
||||
val chapterName: String,
|
||||
@SerializedName("collect")
|
||||
val collect: Boolean,
|
||||
@SerializedName("courseId")
|
||||
val courseId: Int,
|
||||
@SerializedName("desc")
|
||||
val desc: String,
|
||||
@SerializedName("descMd")
|
||||
val descMd: String,
|
||||
@SerializedName("envelopePic")
|
||||
val envelopePic: String,
|
||||
@SerializedName("fresh")
|
||||
val fresh: Boolean,
|
||||
@SerializedName("host")
|
||||
val host: String,
|
||||
@SerializedName("id")
|
||||
val id: Int,
|
||||
@SerializedName("isAdminAdd")
|
||||
val isAdminAdd: Boolean,
|
||||
@SerializedName("link")
|
||||
val link: String,
|
||||
@SerializedName("niceDate")
|
||||
val niceDate: String,
|
||||
@SerializedName("niceShareDate")
|
||||
val niceShareDate: String,
|
||||
@SerializedName("origin")
|
||||
val origin: String,
|
||||
@SerializedName("prefix")
|
||||
val prefix: String,
|
||||
@SerializedName("projectLink")
|
||||
val projectLink: String,
|
||||
@SerializedName("publishTime")
|
||||
val publishTime: Long,
|
||||
@SerializedName("realSuperChapterId")
|
||||
val realSuperChapterId: Int,
|
||||
@SerializedName("selfVisible")
|
||||
val selfVisible: Int,
|
||||
@SerializedName("shareDate")
|
||||
val shareDate: Long,
|
||||
@SerializedName("shareUser")
|
||||
val shareUser: String,
|
||||
@SerializedName("superChapterId")
|
||||
val superChapterId: Int,
|
||||
@SerializedName("superChapterName")
|
||||
val superChapterName: String,
|
||||
@SerializedName("tags")
|
||||
val tags: List<Tag>,
|
||||
@SerializedName("title")
|
||||
val title: String,
|
||||
@SerializedName("type")
|
||||
val type: Int,
|
||||
@SerializedName("userId")
|
||||
val userId: Int,
|
||||
@SerializedName("visible")
|
||||
val visible: Int,
|
||||
@SerializedName("zan")
|
||||
val zan: Int
|
||||
) {
|
||||
@Keep
|
||||
data class Tag(
|
||||
@SerializedName("name")
|
||||
val name: String,
|
||||
@SerializedName("url")
|
||||
val url: String
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.postliu.wanandroid.model.entity
|
||||
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.zj.banner.model.BaseBannerBean
|
||||
|
||||
@Keep
|
||||
data class BannerEntity(
|
||||
@SerializedName("desc")
|
||||
val desc: String = "",
|
||||
@SerializedName("id")
|
||||
val id: Int = -1,
|
||||
@SerializedName("imagePath")
|
||||
val imagePath: Any,
|
||||
@SerializedName("isVisible")
|
||||
val isVisible: Int = 1,
|
||||
@SerializedName("order")
|
||||
val order: Int = 0,
|
||||
@SerializedName("title")
|
||||
val title: String = "",
|
||||
@SerializedName("type")
|
||||
val type: Int = 1,
|
||||
@SerializedName("url")
|
||||
val url: String = ""
|
||||
) : BaseBannerBean() {
|
||||
override val data: Any
|
||||
get() = imagePath
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.postliu.wanandroid.model.entity
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
@Keep
|
||||
data class HomeArticleEntity(
|
||||
@SerializedName("curPage")
|
||||
val curPage: Int,
|
||||
@SerializedName("datas")
|
||||
val datas: List<ArticleEntity>,
|
||||
@SerializedName("offset")
|
||||
val offset: Int,
|
||||
@SerializedName("over")
|
||||
val over: Boolean,
|
||||
@SerializedName("pageCount")
|
||||
val pageCount: Int,
|
||||
@SerializedName("size")
|
||||
val size: Int,
|
||||
@SerializedName("total")
|
||||
val total: Int
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.postliu.wanandroid.model.entity
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import androidx.annotation.Keep
|
||||
|
||||
@Keep
|
||||
data class LoginUserEntity(
|
||||
@SerializedName("admin")
|
||||
val admin: Boolean,
|
||||
@SerializedName("chapterTops")
|
||||
val chapterTops: List<Any>,
|
||||
@SerializedName("coinCount")
|
||||
val coinCount: Int,
|
||||
@SerializedName("collectIds")
|
||||
val collectIds: List<Int>,
|
||||
@SerializedName("email")
|
||||
val email: String,
|
||||
@SerializedName("icon")
|
||||
val icon: String,
|
||||
@SerializedName("id")
|
||||
val id: Int,
|
||||
@SerializedName("nickname")
|
||||
val nickname: String,
|
||||
@SerializedName("password")
|
||||
val password: String,
|
||||
@SerializedName("publicName")
|
||||
val publicName: String,
|
||||
@SerializedName("token")
|
||||
val token: String,
|
||||
@SerializedName("type")
|
||||
val type: Int,
|
||||
@SerializedName("username")
|
||||
val username: String
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.postliu.wanandroid.ui.home
|
||||
|
||||
sealed class HomeAction {
|
||||
object Refresh : HomeAction()
|
||||
data class Collect(val id: Int) : HomeAction()
|
||||
data class ToDetails(val id: Int) : HomeAction()
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.postliu.wanandroid.ui.home
|
||||
|
||||
import com.postliu.wanandroid.common.launchPagingFromZeroFlow
|
||||
import com.postliu.wanandroid.model.ApiService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import javax.inject.Inject
|
||||
|
||||
class HomeRepository @Inject constructor(
|
||||
private val apiService: ApiService
|
||||
) {
|
||||
|
||||
/**
|
||||
* 首页置顶文章
|
||||
*
|
||||
*/
|
||||
fun stickyPostsArticle() = flow {
|
||||
val result = apiService.stickyPostsArticle()
|
||||
emit(result.result)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
/**
|
||||
* 首页文章
|
||||
*
|
||||
*/
|
||||
fun homeArticle() = launchPagingFromZeroFlow { page ->
|
||||
val result = apiService.homeArticle(page)
|
||||
println("数据:${result.errorCode}->${result.errorMsg}-->${result.data.curPage}")
|
||||
result.data.datas
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package com.postliu.wanandroid.ui.home
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import com.postliu.wanandroid.common.GsonUtils
|
||||
import com.postliu.wanandroid.common.Routes
|
||||
import com.postliu.wanandroid.model.entity.ArticleEntity
|
||||
import com.postliu.wanandroid.model.entity.BannerEntity
|
||||
import com.postliu.wanandroid.ui.theme.WanAndroidTheme
|
||||
import com.postliu.wanandroid.widgets.RefreshPagingList
|
||||
import com.zj.banner.BannerPager
|
||||
import com.zj.banner.ui.config.BannerConfig
|
||||
|
||||
fun NavGraphBuilder.home(navController: NavController) {
|
||||
composable(Routes.Home) {
|
||||
val context = LocalContext.current
|
||||
val viewModel: HomeViewModel = hiltViewModel()
|
||||
val homeArticleState = viewModel.viewState.article.collectAsLazyPagingItems()
|
||||
HomePage(articleList = homeArticleState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomePage(
|
||||
bannerList: List<BannerEntity> = emptyList(),
|
||||
articleList: LazyPagingItems<ArticleEntity>
|
||||
) {
|
||||
Scaffold(modifier = Modifier.fillMaxSize(), topBar = {
|
||||
TopAppBar(title = {
|
||||
Text(text = "首页")
|
||||
}, navigationIcon = {
|
||||
IconButton(onClick = {}) {
|
||||
Icon(imageVector = Icons.Default.Menu, contentDescription = null)
|
||||
}
|
||||
}, modifier = Modifier.fillMaxWidth(), actions = {
|
||||
IconButton(onClick = {}) {
|
||||
Icon(imageVector = Icons.Default.Search, contentDescription = null)
|
||||
}
|
||||
}, backgroundColor = MaterialTheme.colors.primary)
|
||||
}) { paddingValues ->
|
||||
RefreshPagingList(
|
||||
paddingValues = paddingValues,
|
||||
lazyPagingItems = articleList,
|
||||
itemContent = {
|
||||
items(articleList) { articleEntity ->
|
||||
articleEntity?.let { HomeArticleView(articleEntity = it) }
|
||||
Divider()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomeArticleView(articleEntity: ArticleEntity) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colors.background)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.Top) {
|
||||
if (articleEntity.fresh) {
|
||||
Text(
|
||||
text = "新",
|
||||
color = Color.Red,
|
||||
style = MaterialTheme.typography.h1,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp, top = 3.dp)
|
||||
.border(1.dp, Color.Red, MaterialTheme.shapes.small)
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = articleEntity.title,
|
||||
maxLines = 2,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.body1,
|
||||
)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
articleEntity.tags.map {
|
||||
Text(
|
||||
text = it.name,
|
||||
color = Color.Blue,
|
||||
style = MaterialTheme.typography.h1,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp, top = 3.dp)
|
||||
.border(1.dp, Color.Blue, MaterialTheme.shapes.small)
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
SpanStyle().copy(
|
||||
color = Color.Gray,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
) {
|
||||
if (articleEntity.superChapterName.isNotEmpty()) {
|
||||
append(articleEntity.superChapterName)
|
||||
append("\t/\t")
|
||||
}
|
||||
append(articleEntity.author.ifBlank { articleEntity.shareUser })
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.h1,
|
||||
color = Color.Gray
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = articleEntity.niceDate,
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = {
|
||||
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Favorite,
|
||||
contentDescription = null,
|
||||
tint = if (articleEntity.collect) Color.Red else Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Banner(
|
||||
dataList: List<BannerEntity>,
|
||||
config: BannerConfig = BannerConfig(),
|
||||
) {
|
||||
BannerPager(
|
||||
onBannerClick = {},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(2f),
|
||||
items = dataList,
|
||||
config = config,
|
||||
indicatorGravity = Alignment.BottomCenter,
|
||||
indicatorIsVertical = true
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun HomeArticleViewPreview() {
|
||||
WanAndroidTheme {
|
||||
val json = "{\n" +
|
||||
"\"adminAdd\": false,\n" +
|
||||
"\"apkLink\": \"\",\n" +
|
||||
"\"audit\": 1,\n" +
|
||||
"\"author\": \"郭霖\",\n" +
|
||||
"\"canEdit\": false,\n" +
|
||||
"\"chapterId\": 409,\n" +
|
||||
"\"chapterName\": \"郭霖\",\n" +
|
||||
"\"collect\": true,\n" +
|
||||
"\"courseId\": 13,\n" +
|
||||
"\"desc\": \"\",\n" +
|
||||
"\"descMd\": \"\",\n" +
|
||||
"\"envelopePic\": \"\",\n" +
|
||||
"\"fresh\": true,\n" +
|
||||
"\"host\": \"\",\n" +
|
||||
"\"id\": 24838,\n" +
|
||||
"\"isAdminAdd\": false,\n" +
|
||||
"\"link\": \"https://mp.weixin.qq.com/s/UmBIVXqKXrjtzSdmUn41pA\",\n" +
|
||||
"\"niceDate\": \"1天前\",\n" +
|
||||
"\"niceShareDate\": \"15小时前\",\n" +
|
||||
"\"origin\": \"\",\n" +
|
||||
"\"prefix\": \"\",\n" +
|
||||
"\"projectLink\": \"\",\n" +
|
||||
"\"publishTime\": 1667232000000,\n" +
|
||||
"\"realSuperChapterId\": 407,\n" +
|
||||
"\"selfVisible\": 0,\n" +
|
||||
"\"shareDate\": 1667314443000,\n" +
|
||||
"\"shareUser\": \"\",\n" +
|
||||
"\"superChapterId\": 408,\n" +
|
||||
"\"superChapterName\": \"公众号\",\n" +
|
||||
"\"tags\": [\n" +
|
||||
"{\n" +
|
||||
"\"name\": \"公众号\",\n" +
|
||||
"\"url\": \"/wxarticle/list/409/1\"\n" +
|
||||
"}\n" +
|
||||
"],\n" +
|
||||
"\"title\": \"Kotlin | 这些隐藏的内存陷阱,你应该熟记于心\",\n" +
|
||||
"\"type\": 0,\n" +
|
||||
"\"userId\": -1,\n" +
|
||||
"\"visible\": 1,\n" +
|
||||
"\"zan\": 0\n" +
|
||||
"}"
|
||||
val articleEntity = with(GsonUtils) {
|
||||
fromJson(json = json, ArticleEntity::class.java)
|
||||
}
|
||||
HomeArticleView(articleEntity = articleEntity)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.postliu.wanandroid.ui.home
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import com.postliu.wanandroid.model.entity.ArticleEntity
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(
|
||||
private val repository: HomeRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val article by lazy {
|
||||
repository.homeArticle().cachedIn(viewModelScope)
|
||||
}
|
||||
|
||||
var viewState by mutableStateOf(HomeViewState(article = article))
|
||||
private set
|
||||
|
||||
fun homeArticle() = viewModelScope.launch {
|
||||
repository.homeArticle().cachedIn(viewModelScope)
|
||||
}
|
||||
}
|
||||
|
||||
data class HomeViewState(
|
||||
val article: Flow<PagingData<ArticleEntity>>
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.postliu.wanandroid.ui.login
|
||||
|
||||
sealed class LoginAction {
|
||||
data class Login(val userName: String, val password: String) : LoginAction()
|
||||
}
|
||||
|
||||
sealed class RegisterAction {
|
||||
data class Register(val userName: String, val password: String, val rePassword: String) :
|
||||
RegisterAction()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.postliu.wanandroid.ui.login
|
||||
|
||||
import com.postliu.wanandroid.common.UIResult
|
||||
import com.postliu.wanandroid.model.ApiService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import javax.inject.Inject
|
||||
|
||||
class LoginRepository @Inject constructor(
|
||||
private val apiService: ApiService
|
||||
) {
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @param userName 用户名
|
||||
* @param password 登录密码
|
||||
*/
|
||||
fun login(userName: String, password: String) = flow {
|
||||
val result = apiService.login(userName, password)
|
||||
if (result.success) {
|
||||
emit(UIResult.Success(result.result))
|
||||
} else {
|
||||
emit(UIResult.Failed(result.errorCode, result.errorMsg))
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*
|
||||
* @param userName 用户名
|
||||
* @param password 登录密码
|
||||
* @param rePassword 确认密码
|
||||
*/
|
||||
fun register(userName: String, password: String, rePassword: String) = flow {
|
||||
val result = apiService.register(userName, password, rePassword)
|
||||
if (result.success) {
|
||||
emit(UIResult.Success(result.result))
|
||||
} else {
|
||||
emit(UIResult.Failed(result.errorCode, result.errorMsg))
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
@file:OptIn(ExperimentalLifecycleComposeApi::class)
|
||||
|
||||
package com.postliu.wanandroid.ui.login
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Phone
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.postliu.wanandroid.R
|
||||
import com.postliu.wanandroid.common.Routes
|
||||
import com.postliu.wanandroid.common.UIResult
|
||||
import com.postliu.wanandroid.common.toast
|
||||
import com.postliu.wanandroid.ui.theme.WanAndroidTheme
|
||||
|
||||
fun NavGraphBuilder.login(navController: NavController) {
|
||||
composable(Routes.Login) {
|
||||
val context = LocalContext.current
|
||||
val viewModel: LoginViewModel = hiltViewModel()
|
||||
val loginState by viewModel.loginState.collectAsStateWithLifecycle(initialValue = null)
|
||||
LoginPage(popBackStack = {
|
||||
navController.popBackStack()
|
||||
}, login = { userName, password ->
|
||||
viewModel.dispatch(LoginAction.Login(userName, password))
|
||||
}, toRegister = { navController.navigate(Routes.Register) })
|
||||
LaunchedEffect(key1 = loginState, block = {
|
||||
loginState?.let {
|
||||
when (val data = it) {
|
||||
is UIResult.Loading -> {
|
||||
println("Loading")
|
||||
}
|
||||
|
||||
is UIResult.Throwable -> {
|
||||
println("Throwable")
|
||||
context.toast(data.throwable.message)
|
||||
}
|
||||
|
||||
is UIResult.Failed -> {
|
||||
println("Failed")
|
||||
context.toast(data.message)
|
||||
}
|
||||
|
||||
is UIResult.Success -> {
|
||||
println("登录成功")
|
||||
context.toast("登录成功")
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginPage(
|
||||
popBackStack: () -> Unit = {},
|
||||
login: (String, String) -> Unit = { _, _ -> },
|
||||
toRegister: () -> Unit = {}
|
||||
) {
|
||||
println("登录页面")
|
||||
var userName by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
Scaffold(topBar = {
|
||||
TopAppBar(title = { Text(text = "登录") }, navigationIcon = {
|
||||
IconButton(onClick = popBackStack) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack, contentDescription = null
|
||||
)
|
||||
}
|
||||
}, backgroundColor = MaterialTheme.colors.primary)
|
||||
}) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.login_logo),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
shape = CircleShape
|
||||
)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = userName,
|
||||
onValueChange = { userName = it },
|
||||
label = { Text(text = "登录用户名") },
|
||||
leadingIcon = { Icon(Icons.Default.Phone, contentDescription = null) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Phone, imeAction = ImeAction.Next
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(text = "登录密码") },
|
||||
leadingIcon = { Icon(imageVector = Icons.Default.Lock, contentDescription = null) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Password, imeAction = ImeAction.Go
|
||||
),
|
||||
keyboardActions = KeyboardActions(onGo = {
|
||||
login.invoke(userName, password)
|
||||
})
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
login.invoke(userName, password)
|
||||
}, modifier = Modifier
|
||||
.padding(horizontal = 50.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(text = "登录")
|
||||
}
|
||||
TextButton(
|
||||
onClick = { toRegister.invoke() },
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.align(Alignment.Start),
|
||||
) {
|
||||
Text(text = "没有账号?点击注册")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoginPagePreview() {
|
||||
WanAndroidTheme {
|
||||
LoginPage()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.postliu.wanandroid.ui.login
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.postliu.wanandroid.common.UIResult
|
||||
import com.postliu.wanandroid.model.entity.LoginUserEntity
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LoginViewModel @Inject constructor(
|
||||
private val repository: LoginRepository
|
||||
) : ViewModel() {
|
||||
|
||||
fun dispatch(action: LoginAction) = when (action) {
|
||||
is LoginAction.Login -> {
|
||||
login(userName = action.userName, password = action.password)
|
||||
}
|
||||
}
|
||||
|
||||
private val mLoginState = MutableSharedFlow<UIResult<LoginUserEntity>>()
|
||||
var loginState = mLoginState.asSharedFlow()
|
||||
|
||||
private fun login(userName: String, password: String) = viewModelScope.launch {
|
||||
repository.login(userName, password).onStart {
|
||||
mLoginState.emit(UIResult.Loading)
|
||||
}.catch {
|
||||
mLoginState.emit(UIResult.Throwable(it))
|
||||
}.collectLatest {
|
||||
mLoginState.emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
@file:OptIn(ExperimentalLifecycleComposeApi::class)
|
||||
|
||||
package com.postliu.wanandroid.ui.login
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Phone
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.postliu.wanandroid.R
|
||||
import com.postliu.wanandroid.common.Routes
|
||||
import com.postliu.wanandroid.common.UIResult
|
||||
import com.postliu.wanandroid.common.toast
|
||||
import com.postliu.wanandroid.ui.theme.WanAndroidTheme
|
||||
|
||||
fun NavGraphBuilder.register(navController: NavController) {
|
||||
composable(Routes.Register) {
|
||||
val context = LocalContext.current
|
||||
val viewModel: RegisterViewModel = hiltViewModel()
|
||||
val registerState by viewModel.register.collectAsStateWithLifecycle(initialValue = null)
|
||||
// 添加页面
|
||||
RegisterPage(popBackStack = {
|
||||
navController.popBackStack()
|
||||
}, register = { userName, password, rePassword ->
|
||||
viewModel.dispatch(RegisterAction.Register(userName, password, rePassword))
|
||||
})
|
||||
// 监听状态
|
||||
LaunchedEffect(key1 = registerState, block = {
|
||||
registerState?.let {
|
||||
when (it) {
|
||||
is UIResult.Loading -> {
|
||||
|
||||
}
|
||||
|
||||
is UIResult.Throwable -> {
|
||||
context.toast(it.throwable)
|
||||
}
|
||||
|
||||
is UIResult.Failed -> {
|
||||
context.toast(it.message)
|
||||
}
|
||||
|
||||
is UIResult.Success -> {
|
||||
context.toast("注册成功")
|
||||
navController.popBackStack(Routes.Home, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RegisterPage(
|
||||
popBackStack: () -> Unit = {},
|
||||
register: (String, String, String) -> Unit = { _, _, _ -> }
|
||||
) {
|
||||
println("进入注册页面")
|
||||
var userName by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var rePassword by remember { mutableStateOf("") }
|
||||
Scaffold(topBar = {
|
||||
TopAppBar(title = { Text(text = "注册登录") }, navigationIcon = {
|
||||
IconButton(onClick = popBackStack) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack, contentDescription = null
|
||||
)
|
||||
}
|
||||
}, backgroundColor = MaterialTheme.colors.primary)
|
||||
}, backgroundColor = Color.White) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.login_logo),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
shape = CircleShape
|
||||
)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = userName,
|
||||
onValueChange = { userName = it },
|
||||
label = { Text(text = "注册用户名") },
|
||||
leadingIcon = { Icon(Icons.Default.Phone, contentDescription = null) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Phone, imeAction = ImeAction.Next
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(text = "登录密码") },
|
||||
leadingIcon = { Icon(imageVector = Icons.Default.Lock, contentDescription = null) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Password, imeAction = ImeAction.Go
|
||||
),
|
||||
keyboardActions = KeyboardActions(onGo = {
|
||||
register.invoke(userName, password, rePassword)
|
||||
})
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = rePassword,
|
||||
onValueChange = { rePassword = it },
|
||||
label = { Text(text = "确认登录密码") },
|
||||
leadingIcon = { Icon(imageVector = Icons.Default.Lock, contentDescription = null) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Password, imeAction = ImeAction.Go
|
||||
),
|
||||
keyboardActions = KeyboardActions(onGo = {
|
||||
register.invoke(userName, password, rePassword)
|
||||
})
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
register.invoke(userName, password, rePassword)
|
||||
}, modifier = Modifier
|
||||
.padding(horizontal = 50.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(text = "注册")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RegisterPagePreview() {
|
||||
WanAndroidTheme {
|
||||
RegisterPage()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.postliu.wanandroid.ui.login
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.postliu.wanandroid.common.UIResult
|
||||
import com.postliu.wanandroid.model.entity.LoginUserEntity
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class RegisterViewModel @Inject constructor(
|
||||
private val repository: LoginRepository
|
||||
) : ViewModel() {
|
||||
|
||||
fun dispatch(action: RegisterAction) = when (action) {
|
||||
is RegisterAction.Register -> {
|
||||
register(action.userName, action.password, action.rePassword)
|
||||
}
|
||||
}
|
||||
|
||||
private val mRegister = MutableSharedFlow<UIResult<LoginUserEntity>>()
|
||||
val register = mRegister.asSharedFlow()
|
||||
|
||||
private fun register(
|
||||
userName: String,
|
||||
password: String,
|
||||
rePassword: String
|
||||
) = viewModelScope.launch {
|
||||
repository.register(userName, password, rePassword).onStart {
|
||||
mRegister.emit(UIResult.Loading)
|
||||
}.catch {
|
||||
mRegister.emit(UIResult.Throwable(it))
|
||||
}.collectLatest {
|
||||
mRegister.emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package com.postliu.wanandroid.ui.main
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.BottomNavigation
|
||||
import androidx.compose.material.BottomNavigationItem
|
||||
import androidx.compose.material.DrawerValue
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ModalDrawer
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.postliu.wanandroid.common.Routes
|
||||
import com.postliu.wanandroid.ui.home.home
|
||||
import com.postliu.wanandroid.ui.login.login
|
||||
import com.postliu.wanandroid.ui.login.register
|
||||
import com.postliu.wanandroid.ui.official.official
|
||||
import com.postliu.wanandroid.ui.project.project
|
||||
import com.postliu.wanandroid.ui.square.square
|
||||
import com.postliu.wanandroid.ui.system.system
|
||||
import com.postliu.wanandroid.ui.theme.WanAndroidTheme
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun MainPage() {
|
||||
val navController: NavHostController = rememberNavController()
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
ModalDrawer(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
DrawerContent() {
|
||||
navController.navigate(Routes.Login)
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
}
|
||||
}
|
||||
},
|
||||
scrimColor = Color(0x50000000),
|
||||
content = {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.navigationBarsPadding(),
|
||||
bottomBar = {
|
||||
when (currentDestination?.route) {
|
||||
Routes.Home -> BottomNavBarView(navController = navController)
|
||||
Routes.Square -> BottomNavBarView(navController = navController)
|
||||
Routes.Official -> BottomNavBarView(navController = navController)
|
||||
Routes.System -> BottomNavBarView(navController = navController)
|
||||
Routes.Project -> BottomNavBarView(navController = navController)
|
||||
}
|
||||
}) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Routes.Home,
|
||||
modifier = Modifier.padding(it)
|
||||
) {
|
||||
home(navController)
|
||||
square(navController)
|
||||
official(navController)
|
||||
system(navController)
|
||||
project(navController)
|
||||
login(navController)
|
||||
register(navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DrawerContent(
|
||||
loginState: Boolean = false,
|
||||
loginName: String = "你知道我是谁吗",
|
||||
login: () -> Unit = {},
|
||||
) {
|
||||
val width = with(LocalDensity.current) {
|
||||
(Resources.getSystem().displayMetrics.widthPixels / 4 * 3) / density
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.width(width = width.dp)
|
||||
.fillMaxHeight()
|
||||
.background(MaterialTheme.colors.background),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
rememberAsyncImagePainter(model = "https://avatars.githubusercontent.com/u/28628369?s=40&v=4"),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.size(width.div(3).dp)
|
||||
.clip(CircleShape)
|
||||
.border(1.dp, MaterialTheme.colors.primary, CircleShape),
|
||||
contentScale = ContentScale.FillWidth
|
||||
)
|
||||
if (loginState) {
|
||||
Text(text = loginName)
|
||||
} else {
|
||||
TextButton(onClick = {
|
||||
login.invoke()
|
||||
}) {
|
||||
Text(text = "去登录")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomNavBarView(
|
||||
navController: NavHostController,
|
||||
bottomNav: List<BottomNav> = listOf(
|
||||
BottomNav.Home,
|
||||
BottomNav.Square,
|
||||
BottomNav.Official,
|
||||
BottomNav.System,
|
||||
BottomNav.Project
|
||||
)
|
||||
) {
|
||||
BottomNavigation(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = MaterialTheme.colors.background
|
||||
) {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
bottomNav.forEach { nav ->
|
||||
BottomNavigationItem(
|
||||
selected = currentDestination?.hierarchy?.any { it.route == nav.route } == true,
|
||||
onClick = {
|
||||
println("BottomNavView当前路由 ===> ${currentDestination?.hierarchy?.toList()}")
|
||||
println("当前路由栈 ===> ${navController.graph.nodes}")
|
||||
if (currentDestination?.route != nav.route) {
|
||||
navController.navigate(nav.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = nav.icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = nav.name)
|
||||
},
|
||||
alwaysShowLabel = true,
|
||||
selectedContentColor = MaterialTheme.colors.primary,
|
||||
unselectedContentColor = Color.Gray,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MainPagePreview() {
|
||||
WanAndroidTheme {
|
||||
Column {
|
||||
MainPage()
|
||||
DrawerContent() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.postliu.wanandroid.ui.main
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.postliu.wanandroid.R
|
||||
import com.postliu.wanandroid.common.Routes
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor() : ViewModel() {
|
||||
|
||||
val defaultBottom = listOf(
|
||||
BottomNav.Home,
|
||||
BottomNav.Square,
|
||||
BottomNav.Official,
|
||||
BottomNav.System,
|
||||
BottomNav.Project
|
||||
)
|
||||
}
|
||||
|
||||
sealed class BottomNav(val icon: Int, val name: String, val route: String) {
|
||||
object Home : BottomNav(R.drawable.icon_home, "首页", Routes.Home)
|
||||
object Square : BottomNav(R.drawable.icon_square, "广场", Routes.Square)
|
||||
object Official : BottomNav(R.drawable.icon_official, "公众号", Routes.Official)
|
||||
object System : BottomNav(R.drawable.icon_system, "体系", Routes.System)
|
||||
object Project : BottomNav(R.drawable.icon_project, "项目", Routes.Project)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.postliu.wanandroid.ui.official
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.postliu.wanandroid.common.Routes
|
||||
|
||||
fun NavGraphBuilder.official(navController: NavController) {
|
||||
composable(Routes.Official) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(text = "公众号")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.postliu.wanandroid.ui.project
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.postliu.wanandroid.common.Routes
|
||||
|
||||
fun NavGraphBuilder.project(navController: NavController) {
|
||||
composable(Routes.Project) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(text = "项目")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.postliu.wanandroid.ui.square
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.postliu.wanandroid.common.Routes
|
||||
|
||||
fun NavGraphBuilder.square(navController: NavController) {
|
||||
composable(Routes.Square) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(text = "广场")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.postliu.wanandroid.ui.system
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.postliu.wanandroid.common.Routes
|
||||
|
||||
fun NavGraphBuilder.system(navController: NavController) {
|
||||
composable(Routes.System) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(text = "体系")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.postliu.wanandroid.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.postliu.wanandroid.ui.theme
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Shapes
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Shapes = Shapes(
|
||||
small = RoundedCornerShape(2.dp),
|
||||
medium = RoundedCornerShape(8.dp),
|
||||
large = RoundedCornerShape(12.dp)
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.postliu.wanandroid.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.lightColors
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
private val DarkColorScheme = darkColors(
|
||||
primary = Purple80,
|
||||
secondary = Purple80,
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColors(
|
||||
primary = Purple40,
|
||||
secondary = Purple40,
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun WanAndroidTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
MaterialTheme(
|
||||
colors = colorScheme,
|
||||
typography = Typography,
|
||||
content = content,
|
||||
shapes = Shapes
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.postliu.wanandroid.ui.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,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
h1 = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 10.sp,
|
||||
lineHeight = 0.sp,
|
||||
letterSpacing = 0.sp
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.postliu.wanandroid.widgets
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
|
||||
@Composable
|
||||
fun <T : Any> RefreshPagingList(
|
||||
lazyPagingItems: LazyPagingItems<T>,
|
||||
isRefreshing: Boolean = false,
|
||||
onRefresh: () -> Unit = {},
|
||||
listState: LazyListState = rememberLazyListState(),
|
||||
paddingValues: PaddingValues = PaddingValues(),
|
||||
itemContent: LazyListScope.() -> Unit
|
||||
) {
|
||||
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = false)
|
||||
val error = lazyPagingItems.loadState.refresh is LoadState.Error
|
||||
if (error) {
|
||||
LoadErrorContent {
|
||||
lazyPagingItems.retry()
|
||||
}
|
||||
return
|
||||
}
|
||||
SwipeRefresh(
|
||||
state = swipeRefreshState,
|
||||
onRefresh = {
|
||||
onRefresh.invoke()
|
||||
lazyPagingItems.refresh()
|
||||
}, modifier = Modifier
|
||||
.padding(paddingValues = paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
swipeRefreshState.isRefreshing =
|
||||
((lazyPagingItems.loadState.refresh is LoadState.Loading) || isRefreshing)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
state = listState
|
||||
) {
|
||||
itemContent()
|
||||
if (!swipeRefreshState.isRefreshing) {
|
||||
item {
|
||||
when (lazyPagingItems.loadState.append) {
|
||||
is LoadState.Loading -> {
|
||||
LoadingItem()
|
||||
}
|
||||
|
||||
is LoadState.Error -> {
|
||||
ErrorItem {
|
||||
lazyPagingItems.retry()
|
||||
}
|
||||
}
|
||||
|
||||
is LoadState.NotLoading -> {
|
||||
if (lazyPagingItems.loadState.append.endOfPaginationReached) {
|
||||
NoMoreItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadErrorContent(retry: () -> Unit) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column {
|
||||
Text(text = "请求出错啦!")
|
||||
Button(onClick = retry) {
|
||||
Text(text = "点击重试")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadingItem() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp), contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoMoreItem() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "没有更多数据了!", modifier = Modifier.padding(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorItem(retry: () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(), contentAlignment = Alignment.Center
|
||||
) {
|
||||
Button(onClick = retry) {
|
||||
Text(text = "重试")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:fillColor="#333333"
|
||||
android:pathData="M555.5,118l312.9,224.6A117.3,117.3 0,0 1,917.3 437.9V800c0,64.8 -52.5,117.3 -117.3,117.3H640V746.7c0,-70.7 -57.3,-128 -128,-128s-128,57.3 -128,128v170.7H224c-64.8,0 -117.3,-52.5 -117.3,-117.3V437.9a117.3,117.3 0,0 1,48.9 -95.3l312.9,-224.6a74.7,74.7 0,0 1,87.1 0z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:fillColor="#333333"
|
||||
android:pathData="M512,85.3c235.6,0 426.7,191 426.7,426.7S747.6,938.7 512,938.7 85.3,747.6 85.3,512 276.4,85.3 512,85.3zM655.4,583.1A160,160 0,0 1,512 672a160,160 0,0 1,-143.4 -88.9,32 32,0 1,0 -57.3,28.5A224,224 0,0 0,512 736a224,224 0,0 0,200.7 -124.4,32 32,0 0,0 -57.3,-28.5z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:fillColor="#333333"
|
||||
android:pathData="M512,85.3c235.6,0 426.7,191 426.7,426.7S747.6,938.7 512,938.7 85.3,747.6 85.3,512 276.4,85.3 512,85.3zM661.2,308.2L444.2,386.4a96,96 0,0 0,-57.8 57.8l-78.1,217a42.7,42.7 0,0 0,54.6 54.6l217,-78.1a96,96 0,0 0,57.8 -57.8l78.1,-217a42.7,42.7 0,0 0,-54.6 -54.6zM512,565.3a53.3,53.3 0,1 0,0 -106.7,53.3 53.3,0 0,0 0,106.7z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:fillColor="#333333"
|
||||
android:pathData="M476,544h63.4l8.5,-64h-63.4l-8.5,64zM512,85.3c235.6,0 426.7,191 426.7,426.7S747.6,938.7 512,938.7a424.8,424.8 0,0 1,-219.1 -60.5,2786.6 2786.6,0 0,0 -20.1,-11.8l-104.4,28.5c-23.9,6.5 -45.8,-15.4 -39.3,-39.3l28.4,-104.3c-11,-18.7 -18.2,-31.2 -21.8,-37.9A424.9,424.9 0,0 1,85.3 512C85.3,276.4 276.4,85.3 512,85.3zM601.6,320.3a32,32 0,0 0,-35.9 27.5L556.5,416h-63.4l8,-59.8a32,32 0,0 0,-63.4 -8.4L428.5,416L352,416a32,32 0,0 0,0 64h68l-8.5,64L352,544a32,32 0,0 0,0 64h50.9l-8,59.8a32,32 0,0 0,63.4 8.4L467.5,608h63.4l-8,59.8a32,32 0,0 0,63.4 8.4L595.5,608L672,608a32,32 0,0 0,0 -64h-68l8.5,-64L672,480a32,32 0,0 0,0 -64h-50.9l8,-59.8a32,32 0,0 0,-27.5 -35.9z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:fillColor="#333333"
|
||||
android:pathData="M512,85.3c235.6,0 426.7,191 426.7,426.7S747.6,938.7 512,938.7 85.3,747.6 85.3,512 276.4,85.3 512,85.3zM726.6,404a32,32 0,0 0,-45.2 0.1L544.7,541.1l-81.8,-89.1a32,32 0,0 0,-46.6 -0.6l-119.4,123.7a32,32 0,1 0,46.1 44.4l95.8,-99.3 81.4,88.7a32,32 0,0 0,46.2 1l160.2,-160.7a32,32 0,0 0,-0.1 -45.2z" />
|
||||
</vector>
|
||||
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">WanAndroid</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.WanAndroid" parent="android:Theme.Material.NoActionBar">
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:fitsSystemWindows">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older that API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.postliu.wanandroid
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||