This commit is contained in:
coco
2026-07-03 16:23:31 +08:00
commit 7a4fb0e6ae
1979 changed files with 101570 additions and 0 deletions
@@ -0,0 +1 @@
/build
@@ -0,0 +1,77 @@
plugins {
id("com.android.application")
id("kotlin-android")
}
android {
compileSdk = 30
buildToolsVersion = "30.0.3"
defaultConfig {
applicationId = "me.wcy.wanandroid.compose"
minSdk = 21
targetSdk = 30
versionCode = 1
versionName = "1.0"
vectorDrawables {
useSupportLibrary = true
}
}
signingConfigs {
getByName("debug") {
storeFile = file("debug.keystore")
}
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
debug {
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = rootProject.extra["compose_version"] as String
kotlinCompilerVersion = rootProject.extra["kotlin_version"] as String
}
}
dependencies {
implementation("androidx.core:core-ktx:1.6.0")
implementation("androidx.appcompat:appcompat:1.3.1")
implementation("com.google.android.material:material:1.4.0")
implementation("androidx.compose.ui:ui:${rootProject.extra["compose_version"]}")
implementation("androidx.compose.material:material:${rootProject.extra["compose_version"]}")
implementation("androidx.compose.ui:ui-tooling:${rootProject.extra["compose_version"]}")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
implementation("androidx.activity:activity-compose:1.3.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07")
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.navigation:navigation-compose:2.4.0-alpha06")
implementation("com.google.accompanist:accompanist-pager:0.10.0")
implementation("com.google.accompanist:accompanist-coil:0.10.0")
implementation("com.google.accompanist:accompanist-swiperefresh:0.10.0")
implementation("com.google.accompanist:accompanist-flowlayout:0.10.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.github.wangchenyan.mock-http:mock-http:1.7")
implementation("com.github.franmontiel:PersistentCookieJar:v1.0.1")
}
Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.wcy.wanandroid.compose">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".WanApplication"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@drawable/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Wanandroidcompose"
android:usesCleartextTraffic="true">
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Wanandroidcompose.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,35 @@
package me.wcy.wanandroid.compose
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.pager.ExperimentalPagerApi
import me.wcy.wanandroid.compose.ui.Main
import me.wcy.wanandroid.compose.ui.mine.CollectList
import me.wcy.wanandroid.compose.ui.mine.Login
import me.wcy.wanandroid.compose.ui.mine.Register
import me.wcy.wanandroid.compose.ui.search.Search
import me.wcy.wanandroid.compose.ui.search.SearchResult
import me.wcy.wanandroid.compose.ui.web.Web
@ExperimentalPagerApi
@Composable
fun ComposeNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "main") {
composable("main") { Main(navController) }
composable("web?url={url}") { backStackEntry ->
val url = backStackEntry.arguments?.getString("url") ?: ""
Web(navController, url)
}
composable("login") { Login(navController) }
composable("register") { Register(navController) }
composable("collect") { CollectList(navController) }
composable("search") { Search(navController) }
composable("search_result?keyword={keyword}") { backStackEntry ->
val keyword = backStackEntry.arguments?.getString("keyword") ?: ""
SearchResult(navController, keyword)
}
}
}
@@ -0,0 +1,31 @@
package me.wcy.wanandroid.compose
import android.app.Application
import android.content.Context
import android.util.Log
import me.wcy.mockhttp.MockHttp
import me.wcy.mockhttp.MockHttpOptions
import me.wcy.wanandroid.compose.auth.AuthManager
/**
* Created by wcy on 2021/4/1.
*/
class WanApplication : Application() {
companion object {
lateinit var context: Context
}
override fun onCreate() {
super.onCreate()
context = this
AuthManager.init()
val options = MockHttpOptions.Builder()
.setMockServerPort(3000)
.setMockSleepTime(500)
.setLogEnable(true)
.setLogLevel(Log.ERROR)
.build()
MockHttp.get().setMockHttpOptions(options)
MockHttp.get().start(this)
}
}
@@ -0,0 +1,46 @@
package me.wcy.wanandroid.compose.api
import com.franmontiel.persistentcookiejar.PersistentCookieJar
import com.franmontiel.persistentcookiejar.cache.SetCookieCache
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
import me.wcy.mockhttp.MockHttpInterceptor
import me.wcy.wanandroid.compose.WanApplication
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
/**
* Created by wcy on 2021/4/1.
*/
object Api {
private const val BASE_URL = "https://www.wanandroid.com/"
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.addInterceptor(MockHttpInterceptor())
.cookieJar(
PersistentCookieJar(
SetCookieCache(),
SharedPrefsCookiePersistor(WanApplication.context)
)
)
.build()
}
private val api by lazy {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
retrofit.create(IApi::class.java)
}
fun get(): IApi {
return api
}
}
@@ -0,0 +1,26 @@
package me.wcy.wanandroid.compose.api
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Created by wcy on 2021/4/1.
*/
object ApiCaller {
const val TAG = "ApiCaller"
}
suspend inline fun <T> apiCall(crossinline call: suspend CoroutineScope.() -> Response<T>): Response<T> {
return withContext(Dispatchers.IO) {
val res: Response<T>
try {
res = call()
} catch (e: Throwable) {
Log.e(ApiCaller.TAG, "request error", e)
return@withContext Response.fromException(e)
}
return@withContext res
}
}
@@ -0,0 +1,74 @@
package me.wcy.wanandroid.compose.api
import me.wcy.wanandroid.compose.auth.User
import me.wcy.wanandroid.compose.ui.home.model.Article
import me.wcy.wanandroid.compose.ui.home.model.ArticleList
import me.wcy.wanandroid.compose.ui.home.model.HomeBannerData
import me.wcy.wanandroid.compose.ui.search.HotKey
import me.wcy.wanandroid.compose.ui.wechat.model.WeChatAuthor
import retrofit2.http.*
/**
* Created by wcy on 2021/4/1.
*/
interface IApi {
@GET("banner/json")
suspend fun getHomeBanner(): Response<List<HomeBannerData>>
@GET("article/list/{page}/json")
suspend fun getHomeArticleList(@Path("page") page: Int = 0): Response<ArticleList>
@GET("article/top/json")
suspend fun getStickyArticle(): Response<List<Article>>
@GET("user_article/list/{page}/json")
suspend fun getSquareArticleList(@Path("page") page: Int = 0): Response<ArticleList>
@GET("wxarticle/chapters/json")
suspend fun getWeChatAuthorList(): Response<List<WeChatAuthor>>
@GET("wxarticle/list/{id}/{page}/json")
suspend fun getWeChatArticleList(
@Path("id") id: Long,
@Path("page") page: Int = 0
): Response<ArticleList>
@FormUrlEncoded
@POST("user/login")
suspend fun login(
@Field("username") username: String,
@Field("password") password: String
): Response<User>
@FormUrlEncoded
@POST("user/register")
suspend fun register(
@Field("username") username: String,
@Field("password") password: String,
@Field("repassword") repassword: String
): Response<String>
@GET("user/logout/json")
suspend fun logout(): Response<String>
@GET("lg/coin/userinfo/json")
suspend fun getUserCoin(): Response<User>
@GET("lg/collect/list/{page}/json")
suspend fun getCollectArticleList(@Path("page") page: Int = 0): Response<ArticleList>
@POST("lg/collect/{id}/json")
suspend fun collect(@Path("id") id: Long): Response<String>
@POST("lg/uncollect_originId/{id}/json")
suspend fun uncollect(@Path("id") id: Long): Response<String>
@GET("hotkey/json")
suspend fun searchHotKey(): Response<List<HotKey>>
@POST("article/query/{page}/json")
suspend fun search(
@Path("page") page: Int,
@Query("k") keyword: String,
): Response<ArticleList>
}
@@ -0,0 +1,54 @@
package me.wcy.wanandroid.compose.api
import com.google.gson.JsonParseException
import com.google.gson.annotations.SerializedName
import org.apache.http.conn.ConnectTimeoutException
import org.json.JSONException
import retrofit2.HttpException
import java.io.IOException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
/**
* Created by wcy on 2021/4/1.
*/
data class Response<T>(
@SerializedName("errorCode") var code: Int = 0,
@SerializedName("errorMsg") var msg: String? = "",
@SerializedName("data") var data: T? = null
) {
fun isSuccess(): Boolean {
return code == CODE_SUCCESS && data != null
}
fun isSuccessIgnoreData(): Boolean {
return code == CODE_SUCCESS
}
companion object {
const val CODE_SUCCESS = 0
// 网络状态码
const val CODE_NET_ERROR = 4000
const val CODE_TIMEOUT = 4080
const val CODE_JSON_PARSE_ERROR = 4010
const val CODE_SERVER_ERROR = 5000
fun <T> fromException(e: Throwable): Response<T> {
return if (e is HttpException) {
Response(CODE_NET_ERROR, "网络异常(${e.code()},${e.message()})")
} else if (e is UnknownHostException) {
Response(CODE_NET_ERROR, "网络连接失败,请检查后再试")
} else if (e is ConnectTimeoutException || e is SocketTimeoutException) {
Response(CODE_TIMEOUT, "请求超时,请稍后再试")
} else if (e is IOException) {
Response(CODE_NET_ERROR, "网络异常(${e.message})")
} else if (e is JsonParseException || e is JSONException) {
// Json解析失败
Response(CODE_JSON_PARSE_ERROR, "数据解析错误,请稍后再试")
} else {
Response(CODE_SERVER_ERROR, "系统错误(${e.message})")
}
}
}
}
@@ -0,0 +1,63 @@
package me.wcy.wanandroid.compose.auth
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.wcy.wanandroid.compose.api.Api
import me.wcy.wanandroid.compose.api.apiCall
import me.wcy.wanandroid.compose.storage.AppPreferences
object AuthManager {
private var userInternal = MutableLiveData<User>()
val user: LiveData<User> = userInternal
fun init() {
GlobalScope.launch {
withContext(Dispatchers.Main) {
userInternal.value = AppPreferences.getUser()
if (isLogin()) {
updateUserCoin()
}
}
}
}
private suspend fun updateUserCoin() {
val userRes = apiCall { Api.get().getUserCoin() }
if (userRes.isSuccess()) {
val user = userRes.data!!
val rawUser = userInternal.value!!.apply {
coinCount = user.coinCount
level = user.level
rank = user.rank
}
AppPreferences.setUser(rawUser)
userInternal.postValue(rawUser)
} else if (userRes.code == -1001) {
onLogout()
}
}
fun isLogin(): Boolean {
return userInternal.value != null
}
fun onLogin(user: User) {
GlobalScope.launch {
userInternal.postValue(user)
AppPreferences.setUser(user)
updateUserCoin()
}
}
fun onLogout() {
GlobalScope.launch {
userInternal.postValue(null)
AppPreferences.setUser(null)
}
}
}
@@ -0,0 +1,16 @@
package me.wcy.wanandroid.compose.auth
import com.google.gson.annotations.SerializedName
data class User(
@SerializedName("id") val id: Long = 0,
@SerializedName("username") val username: String = "",
@SerializedName("nickname") val nickname: String = "",
@SerializedName("chapterTops") val chapterTops: List<Any> = listOf(),
@SerializedName("coinCount") var coinCount: Int = 0,
@SerializedName("collectIds") val collectIds: List<Int> = listOf(),
@SerializedName("email") val email: String = "",
@SerializedName("icon") val icon: String = "",
@SerializedName("level") var level: String = "",
@SerializedName("rank") var rank: String = ""
)
@@ -0,0 +1,21 @@
package me.wcy.wanandroid.compose.storage
import androidx.datastore.preferences.core.stringPreferencesKey
import me.wcy.wanandroid.compose.WanApplication
import me.wcy.wanandroid.compose.auth.User
object AppPreferences {
private val KEY_USER = stringPreferencesKey("user")
private val dataStore by lazy {
PreferencesDataStore(WanApplication.context, WanApplication.context.packageName + ".app")
}
suspend fun getUser(): User? {
return dataStore.getModel(KEY_USER)
}
suspend fun setUser(user: User?) {
dataStore.putModel(KEY_USER, user)
}
}
@@ -0,0 +1,43 @@
package me.wcy.wanandroid.compose.storage
import androidx.datastore.preferences.core.stringPreferencesKey
import me.wcy.wanandroid.compose.WanApplication
object HistoryPreferences {
private val KEY_HISTORY = stringPreferencesKey("history")
private val dataStore by lazy {
PreferencesDataStore(
WanApplication.context,
WanApplication.context.packageName + ".history"
)
}
suspend fun getHistory(): List<String> {
return dataStore.getList(KEY_HISTORY) ?: listOf()
}
suspend fun addHistory(item: String) {
if (item.isEmpty()) {
return
}
val list = getHistory().toMutableList()
if (list.contains(item)) {
return
}
list.add(0, item)
dataStore.putList(KEY_HISTORY, list)
}
suspend fun removeHistory(item: String) {
if (item.isEmpty()) {
return
}
val list = getHistory().toMutableList()
if (!list.contains(item)) {
return
}
list.remove(item)
dataStore.putList(KEY_HISTORY, list)
}
}
@@ -0,0 +1,98 @@
package me.wcy.wanandroid.compose.storage
import android.content.Context
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStoreFile
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import me.wcy.wanandroid.compose.utils.GsonUtils
import java.io.IOException
/**
* Created by wcy on 2021/2/25.
*/
class PreferencesDataStore(context: Context, name: String) {
private val ds by lazy {
PreferenceDataStoreFactory.create {
context.applicationContext.preferencesDataStoreFile(name)
}
}
suspend fun <T> remove(key: Preferences.Key<T>) =
ds.edit { it.remove(key) }
suspend fun clear() =
ds.edit { it.clear() }
suspend inline fun <reified T> get(key: String): T = get(preferencesKey(key))
suspend inline fun <reified T> get(key: Preferences.Key<T>): T =
get(
key, when (T::class.java) {
Boolean::class.javaObjectType -> false as T
Int::class.javaObjectType -> 0 as T
Long::class.javaObjectType -> 0L as T
Float::class.javaObjectType -> 0F as T
Double::class.javaObjectType -> 0.0 as T
String::class.javaObjectType -> "" as T
else -> throw IllegalArgumentException("不支持的类型")
}
)
suspend fun <T> get(key: Preferences.Key<T>, defValue: T): T =
ds.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}.map { preferences ->
preferences[key] ?: defValue
}.first()
suspend inline fun <reified T> put(key: String, value: T) = put(preferencesKey(key), value)
suspend fun <T> put(key: Preferences.Key<T>, value: T) = ds.edit { it[key] = value }
suspend inline fun <reified T> getModel(key: Preferences.Key<String>): T? {
val json = get(key)
if (json.isNotEmpty()) {
return GsonUtils.fromJson(json)
}
return null
}
suspend inline fun <reified T> putModel(key: Preferences.Key<String>, t: T) {
if (t == null) {
remove(key)
} else {
put(key, GsonUtils.toJson(t))
}
}
suspend inline fun <reified T> getList(key: Preferences.Key<String>): List<T>? {
val json = get(key)
if (json.isNotEmpty()) {
return GsonUtils.fromJsonList(json)
}
return null
}
suspend inline fun <reified T> putList(key: Preferences.Key<String>, list: List<T>) =
putModel(key, list)
@Suppress("UNCHECKED_CAST")
inline fun <reified T> preferencesKey(key: String): Preferences.Key<T> =
when (T::class.java) {
Boolean::class.javaObjectType -> booleanPreferencesKey(key) as Preferences.Key<T>
Int::class.javaObjectType -> intPreferencesKey(key) as Preferences.Key<T>
Long::class.javaObjectType -> longPreferencesKey(key) as Preferences.Key<T>
Float::class.javaObjectType -> floatPreferencesKey(key) as Preferences.Key<T>
Double::class.javaObjectType -> doublePreferencesKey(key) as Preferences.Key<T>
String::class.javaObjectType -> stringPreferencesKey(key) as Preferences.Key<T>
else -> throw IllegalArgumentException("不支持的类型")
}
}
@@ -0,0 +1,22 @@
package me.wcy.wanandroid.compose.theme
import androidx.compose.ui.graphics.Color
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
object Colors {
val white = Color.White
val green = Color(0xFF4CAF50)
val red = Color(0xFFF44336)
val blue = Color(0xFF2196F3)
val main = green
val background = Color(0xFFF4F4F4)
val unselect = Color(0xFF424242)
val titleBar = Color(0xFFFAFAFA)
val bottomBar = Color(0xFFFAFAFA)
val text_h1 = Color(0xFF212121)
val text_h2 = Color(0xFF757575)
}
@@ -0,0 +1,11 @@
package me.wcy.wanandroid.compose.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
@@ -0,0 +1,47 @@
package me.wcy.wanandroid.compose.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 DarkColorPalette = darkColors(
primary = Colors.main,
primaryVariant = Colors.main,
secondary = Colors.main
)
private val LightColorPalette = lightColors(
primary = Colors.main,
primaryVariant = Colors.main,
secondary = Colors.main
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
@Composable
fun WanandroidTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable() () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
@@ -0,0 +1,28 @@
package me.wcy.wanandroid.compose.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,47 @@
package me.wcy.wanandroid.compose.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.launch
import me.wcy.wanandroid.compose.ui.home.Home
import me.wcy.wanandroid.compose.ui.mine.Mine
import me.wcy.wanandroid.compose.ui.square.Square
import me.wcy.wanandroid.compose.ui.wechat.WeChat
import me.wcy.wanandroid.compose.widget.BottomTab
@ExperimentalPagerApi
@Composable
fun Main(navController: NavHostController) {
Column(Modifier.fillMaxSize()) {
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState(pageCount = 4, initialOffscreenLimit = 3)
HorizontalPager(pagerState, Modifier.weight(1f)) { page ->
when (page) {
0 -> {
Home(navController)
}
1 -> {
Square(navController)
}
2 -> {
WeChat(navController)
}
3 -> {
Mine(navController)
}
}
}
BottomTab(pagerState.currentPage) {
scope.launch {
pagerState.scrollToPage(it)
}
}
}
}
@@ -0,0 +1,20 @@
package me.wcy.wanandroid.compose.ui
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.google.accompanist.pager.ExperimentalPagerApi
import me.wcy.wanandroid.compose.ComposeNavigation
import me.wcy.wanandroid.compose.theme.WanandroidTheme
class MainActivity : ComponentActivity() {
@ExperimentalPagerApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WanandroidTheme {
ComposeNavigation()
}
}
}
}
@@ -0,0 +1,198 @@
package me.wcy.wanandroid.compose.ui.home
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.shape.RoundedCornerShape
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi
import me.wcy.wanandroid.compose.R
import me.wcy.wanandroid.compose.auth.AuthManager
import me.wcy.wanandroid.compose.theme.Colors
import me.wcy.wanandroid.compose.ui.home.model.Article
import me.wcy.wanandroid.compose.ui.home.model.HomeBannerData
import me.wcy.wanandroid.compose.ui.home.viewmodel.HomeViewModel
import me.wcy.wanandroid.compose.widget.*
/**
* Created by wcy on 2021/3/31.
*/
@ExperimentalPagerApi
@Composable
fun Home(navController: NavHostController) {
val viewModel: HomeViewModel = viewModel()
Column(
Modifier
.fillMaxSize()
.background(Colors.background)
) {
TitleBar(
title = "首页",
icon = R.drawable.ic_search,
onIconClick = {
navController.navigate("search")
}
)
PageLoading(
loadState = viewModel.pageState,
showLoading = viewModel.showLoading,
onReload = { viewModel.firstLoad() }) {
SwipeRefreshAndLoadLayout(
refreshingState = viewModel.refreshingState,
loadState = viewModel.loadState,
onRefresh = { viewModel.onRefresh() },
onLoad = { viewModel.onLoad() }) {
LazyColumn(
Modifier
.fillMaxSize()
.background(Colors.white)
) {
itemsIndexed(viewModel.list) { index, item ->
if (item is List<*>) {
BannerItem(navController, item as List<HomeBannerData>)
} else if (item is Article) {
ArticleItem(navController, item) {
viewModel.collect(item)
}
Divider(Modifier.padding(16.dp, 0.dp), thickness = 0.5.dp)
}
}
}
}
}
}
}
@ExperimentalPagerApi
@Composable
fun BannerItem(navController: NavHostController, list: List<HomeBannerData>) {
val dataList = list.map {
BannerData(it.title, it.imagePath, it.url)
}
Banner(
navController = navController,
modifier = Modifier
.fillMaxWidth()
.height(220.dp),
dataList = dataList
)
}
@Composable
fun ArticleItem(
navController: NavHostController,
article: Article,
onCollectClick: () -> Unit = {}
) {
Box(modifier = Modifier
.fillMaxWidth()
.clickable {
navController.navigate("web?url=${article.link}")
}) {
Column(
Modifier.padding(16.dp, 10.dp)
) {
Row(Modifier.fillMaxWidth()) {
article.tags.forEach {
Text(
it.name,
Modifier
.align(Alignment.CenterVertically)
.border(0.5.dp, it.getColor(), RoundedCornerShape(3.dp))
.padding(2.dp, 1.dp),
it.getColor(),
10.sp
)
Spacer(
modifier = Modifier
.align(Alignment.CenterVertically)
.width(8.dp)
.height(0.dp)
)
}
Text(
article.getAuthor(),
Modifier
.weight(1f)
.align(Alignment.CenterVertically),
Colors.text_h2,
12.sp
)
Spacer(
modifier = Modifier
.align(Alignment.CenterVertically)
.width(10.dp)
.height(0.dp)
)
Text(
article.niceDate,
Modifier
.align(Alignment.CenterVertically),
Colors.text_h2,
12.sp
)
}
Text(
article.title,
Modifier
.fillMaxWidth()
.padding(top = 4.dp),
Colors.text_h1,
15.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 5.dp)
) {
val chapter = StringBuilder(article.superChapterName)
if (article.superChapterName.isNotEmpty() && article.chapterName.isNotEmpty()) {
chapter.append(" / ")
}
chapter.append(article.chapterName)
Text(
chapter.toString(),
Modifier
.weight(1f)
.align(Alignment.CenterVertically),
Colors.text_h2,
12.sp,
)
val iconRes = if (article.collect) R.drawable.ic_like_fill else R.drawable.ic_like
val tint = if (article.collect) Colors.red else Colors.text_h2
Icon(
painter = painterResource(id = iconRes),
contentDescription = "收藏",
modifier = Modifier
.size(24.dp)
.align(Alignment.CenterVertically)
.clickable {
if (!AuthManager.isLogin()) {
navController.navigate("login")
} else {
onCollectClick.invoke()
}
},
tint = tint
)
}
}
}
}
@@ -0,0 +1,72 @@
package me.wcy.wanandroid.compose.ui.home.model
import androidx.compose.ui.graphics.Color
import com.google.gson.annotations.SerializedName
import me.wcy.wanandroid.compose.theme.Colors
data class ArticleList(
@SerializedName("curPage") val curPage: Int = 0,
@SerializedName("datas") val datas: List<Article> = listOf(),
@SerializedName("offset") val offset: Int = 0,
@SerializedName("over") val over: Boolean = false,
@SerializedName("pageCount") val pageCount: Int = 0,
@SerializedName("size") val size: Int = 0,
@SerializedName("total") val total: Int = 0
)
data class Article(
@SerializedName("id") val id: Long = 0,
@SerializedName("apkLink") val apkLink: String = "",
@SerializedName("audit") val audit: Int = 0,
@SerializedName("author") private val author: String = "",
@SerializedName("canEdit") val canEdit: Boolean = false,
@SerializedName("chapterId") val chapterId: Int = 0,
@SerializedName("chapterName") val chapterName: String = "",
@SerializedName("collect") var collect: Boolean = false,
@SerializedName("courseId") val courseId: Int = 0,
@SerializedName("desc") val desc: String = "",
@SerializedName("descMd") val descMd: String = "",
@SerializedName("envelopePic") val envelopePic: String = "",
@SerializedName("fresh") val fresh: Boolean = false,
@SerializedName("host") val host: String = "",
@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 = 0,
@SerializedName("realSuperChapterId") val realSuperChapterId: Int = 0,
@SerializedName("selfVisible") val selfVisible: Int = 0,
@SerializedName("shareDate") val shareDate: Long = 0,
@SerializedName("shareUser") val shareUser: String = "",
@SerializedName("superChapterId") val superChapterId: Int = 0,
@SerializedName("superChapterName") val superChapterName: String = "",
@SerializedName("tags") val tags: MutableList<ArticleTag> = mutableListOf(),
@SerializedName("title") val title: String = "",
@SerializedName("type") val type: Int = 0,
@SerializedName("userId") val userId: Int = 0,
@SerializedName("visible") val visible: Int = 0,
@SerializedName("zan") val zan: Int = 0,
@SerializedName("originId") val originId: Long = 0
) {
fun getAuthor(): String {
return if (author.isNotEmpty()) author else shareUser
}
}
data class ArticleTag(
@SerializedName("name") val name: String = "",
@SerializedName("url") val url: String = ""
) {
fun getColor(): Color {
return when (name) {
"置顶" -> Colors.red
"本站发布" -> Color(0xFF2196F3)
"问答" -> Color(0xFF00BCD4)
"公众号" -> Color(0xFF4CAF50)
"项目" -> Color(0xFF009688)
else -> Colors.main
}
}
}
@@ -0,0 +1,19 @@
package me.wcy.wanandroid.compose.ui.home.model
import com.google.gson.annotations.SerializedName
/**
* Created by wcy on 2021/4/1.
*/
data class HomeBannerData(
@SerializedName("id") val id: Long = 0,
@SerializedName("desc") val desc: String = "",
@SerializedName("imagePath") val imagePath: String = "",
@SerializedName("isVisible") val isVisible: Int = 0,
@SerializedName("order") val order: Int = 0,
@SerializedName("title") val title: String = "",
@SerializedName("type") val type: Int = 0,
@SerializedName("url") val url: String = ""
)
@@ -0,0 +1,108 @@
package me.wcy.wanandroid.compose.ui.home.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import me.wcy.wanandroid.compose.api.Api
import me.wcy.wanandroid.compose.api.apiCall
import me.wcy.wanandroid.compose.ui.home.model.Article
import me.wcy.wanandroid.compose.ui.home.model.ArticleTag
import me.wcy.wanandroid.compose.ui.mine.viewmodel.CollectViewModel
import me.wcy.wanandroid.compose.widget.LoadState
import me.wcy.wanandroid.compose.widget.Toaster
/**
* Created by wcy on 2021/4/1.
*/
class HomeViewModel : ViewModel() {
var pageState by mutableStateOf(LoadState.LOADING)
var showLoading by mutableStateOf(false)
var list by mutableStateOf(listOf<Any>())
var refreshingState by mutableStateOf(false)
var loadState by mutableStateOf(false)
private var page = 0
init {
firstLoad()
}
fun firstLoad() {
viewModelScope.launch {
page = 0
pageState = LoadState.LOADING
val bannerDeffer = async { apiCall { Api.get().getHomeBanner() } }
val stickDeffer = async { apiCall { Api.get().getStickyArticle() } }
val articleDeffer = async { apiCall { Api.get().getHomeArticleList() } }
val bannerRes = bannerDeffer.await()
val stickyRes = stickDeffer.await()
val articleRes = articleDeffer.await()
if (bannerRes.isSuccess() && articleRes.isSuccess() && stickyRes.isSuccess()) {
pageState = LoadState.SUCCESS
list = mutableListOf<Any>().apply {
add(bannerRes.data!!)
addAll(stickyRes.data!!.onEach {
it.tags.add(0, ArticleTag("置顶"))
})
addAll(articleRes.data!!.datas)
}
} else {
pageState = LoadState.FAIL
}
}
}
fun onRefresh() {
viewModelScope.launch {
page = 0
refreshingState = true
val bannerDeffer = async { apiCall { Api.get().getHomeBanner() } }
val stickDeffer = async { apiCall { Api.get().getStickyArticle() } }
val articleDeffer = async { apiCall { Api.get().getHomeArticleList() } }
val bannerRes = bannerDeffer.await()
val stickyRes = stickDeffer.await()
val articleRes = articleDeffer.await()
if (bannerRes.isSuccess() && articleRes.isSuccess() && stickyRes.isSuccess()) {
list = mutableListOf<Any>().apply {
add(bannerRes.data!!)
addAll(stickyRes.data!!.onEach {
it.tags.add(0, ArticleTag("置顶"))
})
addAll(articleRes.data!!.datas)
}
refreshingState = false
} else {
refreshingState = false
Toaster.show("加载失败")
}
}
}
fun onLoad() {
viewModelScope.launch {
loadState = true
val articleList = apiCall { Api.get().getHomeArticleList(page + 1) }
if (articleList.isSuccess()) {
page++
list = list.toMutableList().apply {
addAll(articleList.data!!.datas)
}
loadState = false
} else {
loadState = false
Toaster.show("加载失败")
}
}
}
fun collect(article: Article) {
viewModelScope.launch {
showLoading = true
CollectViewModel.collect(article)
showLoading = false
}
}
}
@@ -0,0 +1,53 @@
package me.wcy.wanandroid.compose.ui.mine
import androidx.compose.foundation.background
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.itemsIndexed
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import me.wcy.wanandroid.compose.theme.Colors
import me.wcy.wanandroid.compose.ui.home.ArticleItem
import me.wcy.wanandroid.compose.ui.mine.viewmodel.CollectViewModel
import me.wcy.wanandroid.compose.widget.PageLoading
import me.wcy.wanandroid.compose.widget.SwipeRefreshAndLoadLayout
import me.wcy.wanandroid.compose.widget.TitleBar
@Composable
fun CollectList(navController: NavHostController) {
val viewModel: CollectViewModel = viewModel()
Column(
Modifier
.fillMaxSize()
.background(Colors.background)
) {
TitleBar(title = "我的收藏", onBack = { navController.popBackStack() })
PageLoading(
loadState = viewModel.pageState,
onReload = { viewModel.firstLoad() }) {
SwipeRefreshAndLoadLayout(
refreshingState = viewModel.refreshingState,
loadState = viewModel.loadState,
onRefresh = { viewModel.onRefresh() },
onLoad = { viewModel.onLoad() }) {
LazyColumn(
Modifier
.fillMaxSize()
.background(Colors.white)) {
itemsIndexed(viewModel.list) { index, item ->
ArticleItem(navController, item) {
viewModel.uncollect(item)
}
Divider(Modifier.padding(16.dp, 0.dp), thickness = 0.5.dp)
}
}
}
}
}
}
@@ -0,0 +1,100 @@
package me.wcy.wanandroid.compose.ui.mine
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
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.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import me.wcy.wanandroid.compose.theme.Colors
import me.wcy.wanandroid.compose.ui.mine.viewmodel.LoginViewModel
import me.wcy.wanandroid.compose.widget.PageLoading
import me.wcy.wanandroid.compose.widget.TitleBar
@Composable
fun Login(navController: NavHostController) {
val viewModel: LoginViewModel = viewModel()
PageLoading(
modifier = Modifier.background(Colors.background),
showLoading = viewModel.showLoading
) {
Column(Modifier.fillMaxSize()) {
TitleBar(title = "登录", onBack = {
navController.popBackStack()
})
Column(Modifier.fillMaxSize()) {
Spacer(
modifier = Modifier
.height(120.dp)
)
OutlinedTextField(
value = viewModel.username,
onValueChange = { value ->
viewModel.username = value
},
modifier = Modifier.fillMaxWidth(),
label = {
Text(text = "请输入用户名")
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
singleLine = true,
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent
)
)
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
value = viewModel.password,
onValueChange = { value ->
viewModel.password = value
},
modifier = Modifier.fillMaxWidth(),
label = {
Text(text = "请输入密码")
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
singleLine = true,
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent
)
)
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
Spacer(modifier = Modifier.height(50.dp))
Button(
onClick = { viewModel.login(navController) },
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.padding(horizontal = 16.dp),
shape = RoundedCornerShape(percent = 50),
) {
Text(text = "登录", fontSize = 15.sp)
}
Spacer(modifier = Modifier.height(20.dp))
Text(
text = "没有账号?去注册",
modifier = Modifier
.align(Alignment.End)
.padding(horizontal = 16.dp)
.clickable {
navController.navigate("register")
},
color = Colors.text_h2,
fontSize = 15.sp
)
}
}
}
}
@@ -0,0 +1,196 @@
package me.wcy.wanandroid.compose.ui.mine
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.*
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.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import me.wcy.wanandroid.compose.R
import me.wcy.wanandroid.compose.theme.Colors
import me.wcy.wanandroid.compose.ui.mine.viewmodel.MineViewModel
import me.wcy.wanandroid.compose.widget.PageLoading
/**
* Created by wcy on 2021/3/31.
*/
@Composable
fun Mine(navController: NavHostController) {
val viewModel: MineViewModel = viewModel()
PageLoading(
showLoading = viewModel.showLoading,
modifier = Modifier.background(Colors.background)
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(30.dp))
Image(
painter = painterResource(id = R.drawable.android_q),
contentDescription = "",
modifier = Modifier
.width(100.dp)
.height(100.dp)
.clickable {
if (viewModel.user == null) {
navController.navigate("login")
}
}
)
Text(
text = viewModel.user?.nickname ?: "未登录",
modifier = Modifier.clickable {
if (viewModel.user == null) {
navController.navigate("login")
}
},
color = Colors.text_h1
)
Spacer(modifier = Modifier.height(15.dp))
Row {
Box(
modifier = Modifier.background(Colors.green)
) {
Text(
text = "LV" + (viewModel.user?.level ?: ""),
modifier = Modifier
.padding(horizontal = 4.dp, vertical = 2.dp),
style = TextStyle(color = Color.White)
)
}
Spacer(modifier = Modifier.width(5.dp))
Box(
modifier = Modifier.background(Colors.blue)
) {
Text(
text = "排名" + (viewModel.user?.rank ?: ""),
modifier = Modifier
.padding(horizontal = 4.dp, vertical = 2.dp),
style = TextStyle(color = Color.White)
)
}
}
Spacer(modifier = Modifier.height(50.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.White)
.clickable {
if (viewModel.user == null) {
navController.navigate("login")
}
}
) {
Spacer(modifier = Modifier.width(16.dp))
Text(
text = "我的积分",
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1f),
color = Colors.text_h1
)
if (viewModel.user != null) {
Text(
text = viewModel.user!!.coinCount.toString(),
modifier = Modifier
.align(Alignment.CenterVertically),
color = Colors.text_h2
)
}
Spacer(modifier = Modifier.width(16.dp))
}
Divider(color = Colors.background, thickness = 0.5.dp)
Row(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.White)
.clickable {
if (viewModel.user == null) {
navController.navigate("login")
} else {
navController.navigate("collect")
}
}
) {
Spacer(modifier = Modifier.width(16.dp))
Text(
text = "我的收藏",
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1f),
color = Colors.text_h1
)
Icon(
painter = painterResource(id = R.drawable.ic_arrow_right),
contentDescription = "",
modifier = Modifier
.size(20.dp)
.align(Alignment.CenterVertically),
tint = Colors.text_h2
)
Spacer(modifier = Modifier.width(16.dp))
}
Spacer(modifier = Modifier.height(50.dp))
if (viewModel.user != null) {
Button(
onClick = { viewModel.showDialog = true },
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.padding(horizontal = 16.dp),
shape = RoundedCornerShape(percent = 50),
colors = ButtonDefaults.buttonColors(
backgroundColor = Colors.red,
contentColor = Color.White
)
) {
Text(text = "退出登录", fontSize = 15.sp)
}
}
}
if (viewModel.showDialog) {
AlertDialog(
onDismissRequest = {},
confirmButton = {
TextButton(
onClick = {
viewModel.showDialog = false
viewModel.logout()
}
) {
Text("确认")
}
},
dismissButton = {
TextButton(
onClick = {
viewModel.showDialog = false
}
) {
Text("取消")
}
},
title = {
Text(text = "提示")
},
text = {
Text(text = "确认退出登录?")
}
)
}
}
}
@@ -0,0 +1,104 @@
package me.wcy.wanandroid.compose.ui.mine
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import me.wcy.wanandroid.compose.theme.Colors
import me.wcy.wanandroid.compose.ui.mine.viewmodel.RegisterViewModel
import me.wcy.wanandroid.compose.widget.PageLoading
import me.wcy.wanandroid.compose.widget.TitleBar
@Composable
fun Register(navController: NavHostController) {
val viewModel: RegisterViewModel = viewModel()
PageLoading(
modifier = Modifier.background(Colors.background),
showLoading = viewModel.showLoading
) {
Column(Modifier.fillMaxSize()) {
TitleBar(title = "注册", onBack = {
navController.popBackStack()
})
Column(Modifier.fillMaxSize()) {
Spacer(
modifier = Modifier
.height(120.dp)
)
OutlinedTextField(
value = viewModel.username,
onValueChange = { value ->
viewModel.username = value
},
modifier = Modifier.fillMaxWidth(),
label = {
Text(text = "请输入用户名")
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
singleLine = true,
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent
)
)
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
value = viewModel.password,
onValueChange = { value ->
viewModel.password = value
},
modifier = Modifier.fillMaxWidth(),
label = {
Text(text = "请输入密码")
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
singleLine = true,
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent
)
)
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
value = viewModel.repassword,
onValueChange = { value ->
viewModel.repassword = value
},
modifier = Modifier.fillMaxWidth(),
label = {
Text(text = "请确认密码")
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
singleLine = true,
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent
)
)
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
Spacer(modifier = Modifier.height(50.dp))
Button(
onClick = { viewModel.register(navController) },
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.padding(horizontal = 16.dp),
shape = RoundedCornerShape(percent = 50),
) {
Text(text = "注册", fontSize = 15.sp)
}
}
}
}
}
@@ -0,0 +1,113 @@
package me.wcy.wanandroid.compose.ui.mine.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import me.wcy.wanandroid.compose.api.Api
import me.wcy.wanandroid.compose.api.apiCall
import me.wcy.wanandroid.compose.ui.home.model.Article
import me.wcy.wanandroid.compose.widget.LoadState
import me.wcy.wanandroid.compose.widget.Toaster
/**
* Created by wcy on 2021/4/1.
*/
class CollectViewModel : ViewModel() {
var pageState by mutableStateOf(LoadState.LOADING)
var showLoading by mutableStateOf(false)
var list by mutableStateOf(listOf<Article>())
var refreshingState by mutableStateOf(false)
var loadState by mutableStateOf(false)
private var page = 0
init {
firstLoad()
}
fun firstLoad() {
viewModelScope.launch {
page = 0
pageState = LoadState.LOADING
val articleList = apiCall { Api.get().getCollectArticleList() }
if (articleList.isSuccess()) {
pageState = LoadState.SUCCESS
list = articleList.data!!.datas.onEach { it.collect = true }
} else {
pageState = LoadState.FAIL
}
}
}
fun onRefresh() {
viewModelScope.launch {
page = 0
refreshingState = true
val articleList = apiCall { Api.get().getCollectArticleList() }
if (articleList.isSuccess()) {
list = articleList.data!!.datas.onEach { it.collect = true }
refreshingState = false
} else {
refreshingState = false
Toaster.show("加载失败")
}
}
}
fun onLoad() {
viewModelScope.launch {
loadState = true
val articleList = apiCall { Api.get().getCollectArticleList(page + 1) }
if (articleList.isSuccess()) {
page++
list = list.toMutableList().apply {
addAll(articleList.data!!.datas.onEach { it.collect = true })
}
loadState = false
} else {
loadState = false
Toaster.show("加载失败")
}
}
}
fun uncollect(article: Article) {
viewModelScope.launch {
showLoading = true
val res = collect(article) { it.originId }
if (res) {
onRefresh()
}
showLoading = false
}
}
companion object {
suspend fun collect(
article: Article,
getId: (article: Article) -> Long = { it.id }
): Boolean {
val id = getId.invoke(article)
if (article.collect) {
val res = apiCall { Api.get().uncollect(id) }
if (res.isSuccessIgnoreData()) {
article.collect = false
return true
} else {
Toaster.show(res.msg)
}
} else {
val res = apiCall { Api.get().collect(id) }
if (res.isSuccessIgnoreData()) {
article.collect = true
return true
} else {
Toaster.show(res.msg)
}
}
return false
}
}
}
@@ -0,0 +1,42 @@
package me.wcy.wanandroid.compose.ui.mine.viewmodel
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.navigation.NavHostController
import kotlinx.coroutines.launch
import me.wcy.wanandroid.compose.api.Api
import me.wcy.wanandroid.compose.api.apiCall
import me.wcy.wanandroid.compose.auth.AuthManager
import me.wcy.wanandroid.compose.widget.Toaster
class LoginViewModel : ViewModel() {
var showLoading by mutableStateOf(false)
var username by mutableStateOf("")
var password by mutableStateOf("")
fun login(navController: NavHostController) {
viewModelScope.launch {
if (username.isEmpty()) {
Toaster.show("请输入用户名")
return@launch
}
if (password.isEmpty()) {
Toaster.show("请输入密码")
return@launch
}
showLoading = true
val loginRes = apiCall { Api.get().login(username, password) }
showLoading = false
if (loginRes.isSuccess()) {
AuthManager.onLogin(loginRes.data!!)
navController.popBackStack()
Toaster.show("登录成功")
} else {
Toaster.show(loginRes.msg)
}
}
}
}
@@ -0,0 +1,32 @@
package me.wcy.wanandroid.compose.ui.mine.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import me.wcy.wanandroid.compose.api.Api
import me.wcy.wanandroid.compose.api.apiCall
import me.wcy.wanandroid.compose.auth.AuthManager
class MineViewModel : ViewModel() {
var user by mutableStateOf(AuthManager.user.value)
var showLoading by mutableStateOf(false)
var showDialog by mutableStateOf(false)
init {
AuthManager.user.observeForever {
user = it?.copy()
}
}
fun logout() {
viewModelScope.launch {
showLoading = true
apiCall { Api.get().logout() }
showLoading = false
AuthManager.onLogout()
}
}
}
@@ -0,0 +1,45 @@
package me.wcy.wanandroid.compose.ui.mine.viewmodel
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.navigation.NavHostController
import kotlinx.coroutines.launch
import me.wcy.wanandroid.compose.api.Api
import me.wcy.wanandroid.compose.api.apiCall
import me.wcy.wanandroid.compose.widget.Toaster
class RegisterViewModel : ViewModel() {
var showLoading by mutableStateOf(false)
var username by mutableStateOf("")
var password by mutableStateOf("")
var repassword by mutableStateOf("")
fun register(navController: NavHostController) {
viewModelScope.launch {
if (username.isEmpty()) {
Toaster.show("请输入用户名")
return@launch
}
if (password.isEmpty()) {
Toaster.show("请输入密码")
return@launch
}
if (repassword.isEmpty()) {
Toaster.show("请确认密码")
return@launch
}
showLoading = true
val registerRes = apiCall { Api.get().register(username, password, repassword) }
showLoading = false
if (registerRes.isSuccessIgnoreData()) {
navController.popBackStack()
Toaster.show("注册成功")
} else {
Toaster.show(registerRes.msg)
}
}
}
}
@@ -0,0 +1,184 @@
package me.wcy.wanandroid.compose.ui.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.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.Icon
import androidx.compose.material.Text
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.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import com.google.accompanist.flowlayout.FlowRow
import me.wcy.wanandroid.compose.R
import me.wcy.wanandroid.compose.theme.Colors
import me.wcy.wanandroid.compose.ui.search.viewmodel.SearchViewModel
import me.wcy.wanandroid.compose.widget.Toaster
@Composable
fun Search(navController: NavHostController) {
val viewModel: SearchViewModel = viewModel()
Column(
modifier = Modifier
.fillMaxSize()
.background(Colors.background)
) {
Row(
Modifier
.fillMaxWidth()
.height(48.dp)
.background(Colors.titleBar)
) {
Icon(
modifier = Modifier
.align(alignment = Alignment.CenterVertically)
.clickable {
navController.popBackStack()
}
.size(48.dp)
.padding(14.dp),
painter = painterResource(id = R.drawable.ic_back),
contentDescription = "返回",
tint = Colors.text_h1
)
BasicTextField(
value = viewModel.keyword,
onValueChange = {
viewModel.keyword = it
},
modifier = Modifier
.align(alignment = Alignment.CenterVertically)
.padding(start = 16.dp, end = 16.dp)
.weight(1f),
textStyle = TextStyle(fontSize = 16.sp),
singleLine = true
)
Icon(
modifier = Modifier
.align(alignment = Alignment.CenterVertically)
.clickable {
viewModel.keyword = ""
}
.size(48.dp)
.padding(14.dp),
painter = painterResource(id = R.drawable.ic_close),
contentDescription = "清除",
tint = Colors.text_h1
)
Icon(
modifier = Modifier
.align(alignment = Alignment.CenterVertically)
.clickable {
if (viewModel.keyword.isNotEmpty()) {
viewModel.addHistory(viewModel.keyword)
navController.navigate("search_result?keyword=${viewModel.keyword}")
} else {
Toaster.show("请输入关键字")
}
}
.size(48.dp)
.padding(14.dp),
painter = painterResource(id = R.drawable.ic_search),
contentDescription = "",
tint = Colors.text_h1
)
}
Column(
modifier = Modifier
.fillMaxSize()
) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "热门搜索",
modifier = Modifier.padding(horizontal = 16.dp),
color = Colors.text_h1,
fontSize = 16.sp
)
Spacer(modifier = Modifier.height(16.dp))
FlowRow(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
mainAxisSpacing = 8.dp,
crossAxisSpacing = 8.dp,
) {
viewModel.hotKeys.forEach {
Box(
modifier = Modifier
.background(
color = Color(0xFFDDDDDD),
shape = RoundedCornerShape(percent = 50)
)
.clickable {
viewModel.keyword = it.name
viewModel.addHistory(viewModel.keyword)
navController.navigate("search_result?keyword=${viewModel.keyword}")
}
) {
Text(
text = it.name,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
color = Colors.text_h1,
fontSize = 14.sp,
maxLines = 1
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "搜索历史",
modifier = Modifier.padding(horizontal = 16.dp),
color = Colors.text_h1,
fontSize = 16.sp
)
Spacer(modifier = Modifier.height(16.dp))
LazyColumn(modifier = Modifier.fillMaxSize()) {
itemsIndexed(viewModel.history) { index, item ->
Box(modifier = Modifier
.fillMaxWidth()
.clickable {
viewModel.keyword = item
navController.navigate("search_result?keyword=${viewModel.keyword}")
}) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Text(
text = item,
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically),
color = Colors.text_h2,
fontSize = 14.sp,
maxLines = 1
)
Icon(
painter = painterResource(id = R.drawable.ic_close),
contentDescription = "删除",
modifier = Modifier
.size(20.dp)
.clickable {
viewModel.removeHistory(item)
},
tint = Colors.text_h2
)
}
}
}
}
}
}
}
@@ -0,0 +1,58 @@
package me.wcy.wanandroid.compose.ui.search
import androidx.compose.foundation.background
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.itemsIndexed
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import me.wcy.wanandroid.compose.theme.Colors
import me.wcy.wanandroid.compose.ui.home.ArticleItem
import me.wcy.wanandroid.compose.ui.search.viewmodel.SearchResultViewModel
import me.wcy.wanandroid.compose.widget.PageLoading
import me.wcy.wanandroid.compose.widget.SwipeRefreshAndLoadLayout
import me.wcy.wanandroid.compose.widget.TitleBar
@Composable
fun SearchResult(navController: NavHostController, keyword: String) {
val viewModel: SearchResultViewModel = viewModel()
viewModel.setKeyword(keyword)
Column(
Modifier
.fillMaxSize()
.background(Colors.background)
) {
TitleBar(title = keyword, onBack = {
navController.popBackStack()
})
PageLoading(
loadState = viewModel.pageState,
onReload = { viewModel.firstLoad() },
showLoading = viewModel.showLoading
) {
SwipeRefreshAndLoadLayout(
refreshingState = viewModel.refreshingState,
loadState = viewModel.loadState,
onRefresh = { viewModel.onRefresh() },
onLoad = { viewModel.onLoad() }) {
LazyColumn(
Modifier
.fillMaxSize()
.background(Colors.white)) {
itemsIndexed(viewModel.list) { index, item ->
ArticleItem(navController, item) {
viewModel.collect(item)
}
Divider(Modifier.padding(16.dp, 0.dp), thickness = 0.5.dp)
}
}
}
}
}
}
@@ -0,0 +1,9 @@
package me.wcy.wanandroid.compose.ui.search
import com.google.gson.annotations.SerializedName
data class HotKey(
@SerializedName("id") val id: Long = 0,
@SerializedName("link") val link: String = "",
@SerializedName("name") val name: String = ""
)
@@ -0,0 +1,89 @@
package me.wcy.wanandroid.compose.ui.search.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import me.wcy.wanandroid.compose.api.Api
import me.wcy.wanandroid.compose.api.apiCall
import me.wcy.wanandroid.compose.ui.home.model.Article
import me.wcy.wanandroid.compose.ui.mine.viewmodel.CollectViewModel
import me.wcy.wanandroid.compose.widget.LoadState
import me.wcy.wanandroid.compose.widget.Toaster
class SearchResultViewModel : ViewModel() {
var pageState by mutableStateOf(LoadState.LOADING)
var showLoading by mutableStateOf(false)
var list by mutableStateOf(listOf<Article>())
var refreshingState by mutableStateOf(false)
var loadState by mutableStateOf(false)
private var keyword = ""
private var page = 0
fun setKeyword(keyword: String) {
if (this.keyword.isEmpty()) {
this.keyword = keyword
firstLoad()
}
}
fun firstLoad() {
viewModelScope.launch {
page = 0
pageState = LoadState.LOADING
val articleList = apiCall { Api.get().search(page, keyword) }
if (articleList.isSuccess()) {
pageState = LoadState.SUCCESS
if (articleList.data!!.datas.isNotEmpty()) {
list = articleList.data!!.datas
} else {
pageState = LoadState.EMPTY
}
} else {
pageState = LoadState.FAIL
}
}
}
fun onRefresh() {
viewModelScope.launch {
page = 0
refreshingState = true
val articleList = apiCall { Api.get().search(page, keyword) }
if (articleList.isSuccess()) {
list = articleList.data!!.datas
refreshingState = false
} else {
refreshingState = false
Toaster.show("加载失败")
}
}
}
fun onLoad() {
viewModelScope.launch {
loadState = true
val articleList = apiCall { Api.get().search(page + 1, keyword) }
if (articleList.isSuccess()) {
page++
list = list.toMutableList().apply {
addAll(articleList.data!!.datas)
}
loadState = false
} else {
loadState = false
Toaster.show("加载失败")
}
}
}
fun collect(article: Article) {
viewModelScope.launch {
showLoading = true
CollectViewModel.collect(article)
showLoading = false
}
}
}
@@ -0,0 +1,58 @@
package me.wcy.wanandroid.compose.ui.search.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import me.wcy.wanandroid.compose.api.Api
import me.wcy.wanandroid.compose.api.apiCall
import me.wcy.wanandroid.compose.storage.HistoryPreferences
import me.wcy.wanandroid.compose.ui.search.HotKey
class SearchViewModel : ViewModel() {
var keyword by mutableStateOf("")
var hotKeys by mutableStateOf(listOf<HotKey>())
var history by mutableStateOf(listOf<String>())
init {
getHotKey()
getHistory()
}
private fun getHotKey() {
viewModelScope.launch {
val hotKeyRes = apiCall { Api.get().searchHotKey() }
if (hotKeyRes.isSuccess()) {
hotKeys = hotKeyRes.data!!
}
}
}
private fun getHistory() {
viewModelScope.launch {
history = HistoryPreferences.getHistory()
}
}
fun addHistory(item: String) {
viewModelScope.launch {
if (item.isEmpty() || history.contains(item)) {
return@launch
}
history = history.toMutableList().apply { add(0, item) }
HistoryPreferences.addHistory(item)
}
}
fun removeHistory(item: String) {
viewModelScope.launch {
if (item.isEmpty() || !history.contains(item)) {
return@launch
}
history = history.toMutableList().apply { remove(item) }
HistoryPreferences.removeHistory(item)
}
}
}
@@ -0,0 +1,67 @@
package me.wcy.wanandroid.compose.ui.square
import androidx.compose.foundation.background
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.itemsIndexed
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import me.wcy.wanandroid.compose.R
import me.wcy.wanandroid.compose.theme.Colors
import me.wcy.wanandroid.compose.ui.home.ArticleItem
import me.wcy.wanandroid.compose.ui.square.viewmodel.SquareViewModel
import me.wcy.wanandroid.compose.widget.PageLoading
import me.wcy.wanandroid.compose.widget.SwipeRefreshAndLoadLayout
import me.wcy.wanandroid.compose.widget.TitleBar
import me.wcy.wanandroid.compose.widget.Toaster
/**
* Created by wcy on 2021/3/31.
*/
@Composable
fun Square(navController: NavHostController) {
val viewModel: SquareViewModel = viewModel()
Column(
Modifier
.fillMaxSize()
.background(Colors.background)
) {
TitleBar(
title = "广场",
icon = R.drawable.ic_share,
onIconClick = {
Toaster.show("分享")
}
)
PageLoading(
loadState = viewModel.pageState,
onReload = { viewModel.firstLoad() },
showLoading = viewModel.showLoading
) {
SwipeRefreshAndLoadLayout(
refreshingState = viewModel.refreshingState,
loadState = viewModel.loadState,
onRefresh = { viewModel.onRefresh() },
onLoad = { viewModel.onLoad() }) {
LazyColumn(
Modifier
.fillMaxSize()
.background(Colors.white)) {
itemsIndexed(viewModel.list) { index, item ->
ArticleItem(navController, item) {
viewModel.collect(item)
}
Divider(Modifier.padding(16.dp, 0.dp), thickness = 0.5.dp)
}
}
}
}
}
}
@@ -0,0 +1,84 @@
package me.wcy.wanandroid.compose.ui.square.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import me.wcy.wanandroid.compose.api.Api
import me.wcy.wanandroid.compose.api.apiCall
import me.wcy.wanandroid.compose.ui.home.model.Article
import me.wcy.wanandroid.compose.ui.mine.viewmodel.CollectViewModel
import me.wcy.wanandroid.compose.widget.LoadState
import me.wcy.wanandroid.compose.widget.Toaster
/**
* Created by wcy on 2021/4/1.
*/
class SquareViewModel : ViewModel() {
var pageState by mutableStateOf(LoadState.LOADING)
var showLoading by mutableStateOf(false)
var list by mutableStateOf(listOf<Article>())
var refreshingState by mutableStateOf(false)
var loadState by mutableStateOf(false)
private var page = 0
init {
firstLoad()
}
fun firstLoad() {
viewModelScope.launch {
page = 0
pageState = LoadState.LOADING
val articleList = apiCall { Api.get().getSquareArticleList() }
if (articleList.isSuccess()) {
pageState = LoadState.SUCCESS
list = articleList.data!!.datas
} else {
pageState = LoadState.FAIL
}
}
}
fun onRefresh() {
viewModelScope.launch {
page = 0
refreshingState = true
val articleList = apiCall { Api.get().getSquareArticleList() }
if (articleList.isSuccess()) {
list = articleList.data!!.datas
refreshingState = false
} else {
refreshingState = false
Toaster.show("加载失败")
}
}
}
fun onLoad() {
viewModelScope.launch {
loadState = true
val articleList = apiCall { Api.get().getSquareArticleList(page + 1) }
if (articleList.isSuccess()) {
page++
list = list.toMutableList().apply {
addAll(articleList.data!!.datas)
}
loadState = false
} else {
loadState = false
Toaster.show("加载失败")
}
}
}
fun collect(article: Article) {
viewModelScope.launch {
showLoading = true
CollectViewModel.collect(article)
showLoading = false
}
}
}
@@ -0,0 +1,122 @@
package me.wcy.wanandroid.compose.ui.web
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.net.http.SslError
import android.os.Build
import android.text.TextUtils
import android.view.ViewGroup
import android.webkit.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import me.wcy.wanandroid.compose.ui.web.viewmodel.WebViewModel
import me.wcy.wanandroid.compose.widget.TitleBar
@Composable
fun Web(navController: NavHostController, url: String) {
val viewModel: WebViewModel = viewModel()
Column(
Modifier
.fillMaxSize()
.background(Color.White)) {
TitleBar(title = viewModel.title, onBack = {
navController.popBackStack()
})
Box(Modifier.fillMaxSize()) {
AndroidView({ context ->
WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
settings.apply {
domStorageEnabled = true
databaseEnabled = true
setAppCacheEnabled(true)
setAppCachePath(context.cacheDir.absolutePath)
allowFileAccess = false
savePassword = false
javaScriptEnabled = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
}
}
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
url: String?
): Boolean {
if (url == null) {
return false
}
if (url.startsWith("http://", true)
|| url.startsWith("https://", true)
|| url.startsWith("ftp://", true)
) {
view?.loadUrl(url)
} else {
val uri = Uri.parse(url)
if (!TextUtils.isEmpty(uri.scheme) && !TextUtils.isEmpty(uri.host)) {
val intent = Intent(Intent.ACTION_VIEW, uri)
if (isInstall(intent)) {
try {
context.startActivity(intent)
} catch (e: Exception) {
}
}
}
}
return true
}
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?
) {
handler?.proceed()
}
private fun isInstall(intent: Intent): Boolean {
return context.packageManager.queryIntentActivities(
intent,
PackageManager.MATCH_DEFAULT_ONLY
).size > 0
}
}
webChromeClient = object : WebChromeClient() {
override fun onReceivedTitle(view: WebView?, title: String?) {
viewModel.title = title ?: ""
}
override fun onProgressChanged(view: WebView?, newProgress: Int) {
super.onProgressChanged(view, newProgress)
viewModel.progress = newProgress
}
}
loadUrl(url)
}
}, Modifier.fillMaxSize())
if (viewModel.progress < 100) {
LinearProgressIndicator(
progress = viewModel.progress / 100f,
modifier = Modifier
.fillMaxWidth()
.height(2.dp),
backgroundColor = Color.Transparent
)
}
}
}
}
@@ -0,0 +1,11 @@
package me.wcy.wanandroid.compose.ui.web.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
class WebViewModel : ViewModel() {
var title by mutableStateOf("加载中...")
var progress by mutableStateOf(0)
}
@@ -0,0 +1,131 @@
package me.wcy.wanandroid.compose.ui.wechat
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.*
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.launch
import me.wcy.wanandroid.compose.theme.Colors
import me.wcy.wanandroid.compose.ui.home.ArticleItem
import me.wcy.wanandroid.compose.ui.wechat.viewmodel.WeChatTabViewModel
import me.wcy.wanandroid.compose.ui.wechat.viewmodel.WeChatViewModel
import me.wcy.wanandroid.compose.widget.PageLoading
import me.wcy.wanandroid.compose.widget.SwipeLoadLayout
import me.wcy.wanandroid.compose.widget.TitleBar
/**
* Created by wcy on 2021/3/31.
*/
@ExperimentalPagerApi
@Composable
fun WeChat(navController: NavHostController) {
val viewModel: WeChatViewModel = viewModel()
Column(
Modifier
.fillMaxSize()
.background(Colors.background)
) {
TitleBar(title = "公众号")
PageLoading(
loadState = viewModel.pageState,
onReload = { viewModel.getAuthorList() }) {
if (viewModel.authorList.isNotEmpty()) {
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState(
pageCount = viewModel.authorList.size,
initialOffscreenLimit = viewModel.authorList.size - 1
)
Column(Modifier.fillMaxSize()) {
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
modifier = Modifier
.fillMaxWidth(),
backgroundColor = Colors.titleBar,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
modifier = Modifier
.tabIndicatorOffset(tabPositions[pagerState.currentPage])
.padding(start = 20.dp, end = 20.dp),
color = Colors.main
)
},
divider = {}
) {
viewModel.authorList.forEachIndexed { index, weChatAuthor ->
Tab(
modifier = Modifier.padding(vertical = 10.dp),
selected = (index == pagerState.currentPage),
onClick = {
scope.launch {
pagerState.scrollToPage(index)
}
}) {
Text(text = weChatAuthor.name, fontSize = 16.sp)
}
}
}
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
) {
WeChatTab(
navController,
viewModel,
viewModel.authorList[currentPage].id
)
}
}
}
}
}
}
@Composable
fun WeChatTab(navController: NavHostController, viewModel: WeChatViewModel, id: Long) {
var tabViewModel = viewModel.tabViewModelMap[id]
if (tabViewModel == null) {
tabViewModel = WeChatTabViewModel(viewModel.viewModelScope, id)
viewModel.tabViewModelMap.put(id, tabViewModel)
}
Column(Modifier.fillMaxSize()) {
PageLoading(
loadState = tabViewModel.pageState,
onReload = { tabViewModel.firstLoad() },
showLoading = tabViewModel.showLoading
) {
SwipeLoadLayout(
loadState = tabViewModel.loadState,
onLoad = { tabViewModel.loadArticleList() }) {
LazyColumn(
Modifier
.fillMaxSize()
.background(Colors.white)
) {
itemsIndexed(tabViewModel.articleList) { index, item ->
ArticleItem(navController, item) {
tabViewModel.collect(item)
}
Divider(Modifier.padding(16.dp, 0.dp), thickness = 0.5.dp)
}
}
}
}
}
}
@@ -0,0 +1,8 @@
package me.wcy.wanandroid.compose.ui.wechat.model
import com.google.gson.annotations.SerializedName
data class WeChatAuthor(
@SerializedName("id") val id: Long = 0,
@SerializedName("name") val name: String = ""
)
@@ -0,0 +1,64 @@
package me.wcy.wanandroid.compose.ui.wechat.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.wcy.wanandroid.compose.api.Api
import me.wcy.wanandroid.compose.api.apiCall
import me.wcy.wanandroid.compose.ui.home.model.Article
import me.wcy.wanandroid.compose.ui.mine.viewmodel.CollectViewModel
import me.wcy.wanandroid.compose.widget.LoadState
import me.wcy.wanandroid.compose.widget.Toaster
class WeChatTabViewModel(private val scope: CoroutineScope, private val id: Long) {
var pageState by mutableStateOf(LoadState.LOADING)
var showLoading by mutableStateOf(false)
var articleList by mutableStateOf(listOf<Article>())
var loadState by mutableStateOf(false)
private var page = 0
init {
firstLoad()
}
fun firstLoad() {
scope.launch {
page = 0
pageState = LoadState.LOADING
val res = apiCall { Api.get().getWeChatArticleList(id) }
if (res.isSuccess()) {
pageState = LoadState.SUCCESS
articleList = res.data!!.datas
} else {
pageState = LoadState.FAIL
}
}
}
fun loadArticleList() {
scope.launch {
loadState = true
val res = apiCall { Api.get().getWeChatArticleList(id, page + 1) }
if (res.isSuccess()) {
page++
articleList = articleList.toMutableList().apply {
addAll(res.data!!.datas)
}
loadState = false
} else {
loadState = false
Toaster.show("加载失败")
}
}
}
fun collect(article: Article) {
scope.launch {
showLoading = true
CollectViewModel.collect(article)
showLoading = false
}
}
}
@@ -0,0 +1,36 @@
package me.wcy.wanandroid.compose.ui.wechat.viewmodel
import android.util.LongSparseArray
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import me.wcy.wanandroid.compose.api.Api
import me.wcy.wanandroid.compose.api.apiCall
import me.wcy.wanandroid.compose.ui.wechat.model.WeChatAuthor
import me.wcy.wanandroid.compose.widget.LoadState
class WeChatViewModel : ViewModel() {
var pageState by mutableStateOf(LoadState.LOADING)
var authorList by mutableStateOf(listOf<WeChatAuthor>())
val tabViewModelMap = LongSparseArray<WeChatTabViewModel>()
init {
getAuthorList()
}
fun getAuthorList() {
viewModelScope.launch {
pageState = LoadState.LOADING
val authorListRes = apiCall { Api.get().getWeChatAuthorList() }
if (authorListRes.isSuccess()) {
pageState = LoadState.SUCCESS
authorList = authorListRes.data!!
} else {
pageState = LoadState.FAIL
}
}
}
}
@@ -0,0 +1,62 @@
package me.wcy.wanandroid.compose.utils
import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.JsonParser
/**
* Created by wcy on 2021/2/24.
*/
object GsonUtils {
val gson by lazy { Gson() }
val parser by lazy { JsonParser() }
fun toJson(src: Any): String {
return gson.toJson(src)
}
inline fun <reified T> fromJson(json: String): T? {
return fromJson(json, T::class.java)
}
fun <T> fromJson(json: String, clazz: Class<T>): T? {
try {
return gson.fromJson(json, clazz)
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
inline fun <reified T> fromJson(json: JsonElement): T? {
return fromJson(json, T::class.java)
}
fun <T> fromJson(json: JsonElement, clazz: Class<T>): T? {
try {
return gson.fromJson(json, clazz)
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
inline fun <reified T> fromJsonList(json: String): List<T>? {
return fromJsonList(json, T::class.java)
}
fun <T> fromJsonList(json: String, clazz: Class<T>): List<T>? {
try {
val list = mutableListOf<T>()
val jsonArray = parser.parse(json).asJsonArray
for (item in jsonArray) {
val bean = gson.fromJson(item, clazz)
list.add(bean)
}
return list
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
}
@@ -0,0 +1,129 @@
package me.wcy.wanandroid.compose.widget
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
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.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import com.google.accompanist.coil.rememberCoilPainter
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.launch
import me.wcy.wanandroid.compose.theme.Colors
/**
* Created by wcy on 2021/4/1.
*/
data class BannerData(
val title: String,
val imageUrl: String,
val jumpUrl: String
)
@SuppressLint("CoroutineCreationDuringComposition")
@ExperimentalPagerApi
@Composable
fun Banner(
navController: NavHostController,
modifier: Modifier,
dataList: List<BannerData>
) {
val pagerState =
rememberPagerState(pageCount = dataList.size, initialOffscreenLimit = dataList.size - 1)
val handler = remember {
Handler(Looper.getMainLooper())
}
val scope = rememberCoroutineScope()
handler.removeCallbacksAndMessages(null)
handler.postDelayed(object : Runnable {
override fun run() {
scope.launch {
if (pagerState.pageCount > 0) {
pagerState.animateScrollToPage((pagerState.currentPage + 1) % pagerState.pageCount)
}
}
handler.postDelayed(this, 3000)
}
}, 3000)
HorizontalPager(
state = pagerState,
modifier = modifier
) {
val bannerData = dataList[currentPage]
Box(modifier = Modifier
.fillMaxSize()
.clickable {
navController.navigate("web?url=${bannerData.jumpUrl}")
}) {
Image(
painter = rememberCoilPainter(bannerData.imageUrl),
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
Row(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.background(Color(0xFF60000000))
.padding(16.dp, 4.dp)
) {
Text(
text = bannerData.title,
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically),
color = Colors.white,
fontSize = 15.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(
modifier = Modifier
.width(2.dp)
.height(0.dp)
)
for (i in dataList.indices) {
Spacer(
modifier = Modifier
.width(8.dp)
.height(0.dp)
)
val color =
if (i == pagerState.currentPage) Colors.white else Color.LightGray
Box(
modifier = Modifier
.align(Alignment.CenterVertically)
.width(5.dp)
.height(5.dp)
.clip(CircleShape)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(color)
)
}
}
}
}
}
}
@@ -0,0 +1,99 @@
package me.wcy.wanandroid.compose.widget
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text
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.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.wcy.wanandroid.compose.R
import me.wcy.wanandroid.compose.theme.Colors
/**
* Created by wcy on 2021/3/30.
*/
@Preview
@Composable
fun BottomTabPreview() {
BottomTab(current = 0) {
}
}
@Composable
fun BottomTab(
current: Int,
currentChanged: (Int) -> Unit
) {
Row(
Modifier
.fillMaxWidth()
.background(Colors.bottomBar)
.padding(4.dp, 0.dp)
) {
TabItem(
Modifier
.weight(1f)
.clickable {
currentChanged(0)
},
if (current == 0) R.drawable.ic_tab_home_fill else R.drawable.ic_tab_home,
"首页",
if (current == 0) Colors.main else Colors.unselect
)
TabItem(
Modifier
.weight(1f)
.clickable {
currentChanged(1)
},
if (current == 1) R.drawable.ic_tab_discover_fill else R.drawable.ic_tab_discover,
"广场",
if (current == 1) Colors.main else Colors.unselect
)
TabItem(
Modifier
.weight(1f)
.clickable {
currentChanged(2)
},
if (current == 2) R.drawable.ic_tab_wechat_fill else R.drawable.ic_tab_wechat,
"公众号",
if (current == 2) Colors.main else Colors.unselect
)
TabItem(
Modifier
.weight(1f)
.clickable {
currentChanged(3)
},
if (current == 3) R.drawable.ic_tab_my_fill else R.drawable.ic_tab_my,
"我的",
if (current == 3) Colors.main else Colors.unselect
)
}
}
@Composable
fun TabItem(
modifier: Modifier = Modifier,
@DrawableRes iconResId: Int,
title: String,
tint: Color
) {
Column(
modifier.padding(0.dp, 8.dp, 0.dp, 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(painterResource(iconResId), title, Modifier.size(24.dp), tint)
Text(title, fontSize = 11.sp, color = tint)
}
}
@@ -0,0 +1,77 @@
package me.wcy.wanandroid.compose.widget
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
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.graphics.Color
/**
* Created by wcy on 2021/4/1.
*/
enum class LoadState {
LOADING,
SUCCESS,
FAIL,
EMPTY
}
@Composable
fun PageLoading(
modifier: Modifier = Modifier,
loadState: LoadState = LoadState.SUCCESS,
onReload: () -> Unit = {},
showLoading: Boolean = false,
content: @Composable BoxScope.() -> Unit
) {
Box(modifier = modifier.fillMaxSize()) {
when (loadState) {
LoadState.LOADING -> {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
LoadState.SUCCESS -> {
content.invoke(this)
if (showLoading) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0x80000000))
.clickable { }
) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
color = Color.White
)
}
}
}
LoadState.FAIL -> {
Box(modifier = Modifier
.fillMaxSize()
.clickable { onReload.invoke() }) {
Text(
text = "加载失败,点击重试",
Modifier.align(Alignment.Center)
)
}
}
LoadState.EMPTY -> {
Box(modifier = Modifier
.fillMaxSize()
.clickable { onReload.invoke() }) {
Text(
text = "这里什么都没有",
Modifier.align(Alignment.Center)
)
}
}
}
}
}
@@ -0,0 +1,127 @@
package me.wcy.wanandroid.compose.widget
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
private val LoadDistance = 70.dp
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeLoadLayout(
loadState: Boolean,
onLoad: () -> Unit,
content: @Composable () -> Unit
) {
val loadDistance = with(LocalDensity.current) { LoadDistance.toPx() }
val loadRefreshState = rememberSwipeableState(loadState) { newValue ->
// compare both copies of the swipe state before calling onRefresh(). This is a workaround.
if (newValue && !loadState) onLoad()
true
}
Box(
modifier = Modifier
.nestedScroll(loadRefreshState.LoadPreUpPostDownNestedScrollConnection)
.swipeable(
state = loadRefreshState,
anchors = mapOf(
loadDistance to false,
-loadDistance to true,
),
thresholds = { _, _ -> FractionalThreshold(0.5f) },
orientation = Orientation.Vertical
)
.fillMaxSize()
) {
content()
Box(
Modifier
.align(Alignment.BottomCenter)
.offset { IntOffset(0, loadRefreshState.offset.value.roundToInt()) }
) {
if (loadRefreshState.offset.value != loadDistance) {
Surface(elevation = 10.dp, shape = CircleShape) {
CircularProgressIndicator(
modifier = Modifier
.size(36.dp)
.padding(4.dp)
)
}
}
}
// TODO (https://issuetracker.google.com/issues/164113834): This state->event trampoline is a
// workaround for a bug in the SwipableState API. Currently, state.value is a duplicated
// source of truth of refreshingState.
LaunchedEffect(loadState) { loadRefreshState.animateTo(loadState) }
}
}
/**
* Temporary workaround for nested scrolling behavior. There is no default implementation for
* pull to refresh yet, this nested scroll connection mimics the behavior.
*/
@ExperimentalMaterialApi
private val <T> SwipeableState<T>.LoadPreUpPostDownNestedScrollConnection: NestedScrollConnection
get() = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
return if (delta > 0 && source == NestedScrollSource.Drag) {
performDrag(delta).toOffset()
} else {
Offset.Zero
}
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
return if (source == NestedScrollSource.Drag) {
performDrag(available.toFloat()).toOffset()
} else {
Offset.Zero
}
}
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = Offset(available.x, available.y).toFloat()
return if (toFling > 0) {
performFling(velocity = toFling)
// since we go to the anchor with tween settling, consume all for the best UX
// available
Velocity.Zero
} else {
Velocity.Zero
}
}
override suspend fun onPostFling(
consumed: Velocity,
available: Velocity
): Velocity {
performFling(velocity = Offset(available.x, available.y).toFloat())
return Velocity.Zero
}
private fun Float.toOffset(): Offset = Offset(0f, this)
private fun Offset.toFloat(): Float = this.y
}
@@ -0,0 +1,234 @@
package me.wcy.wanandroid.compose.widget
import android.util.Log
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
private const val TAG = "SwipeToRefreshAndLoad"
private val RefreshDistance = 80.dp
private val LoadDistance = 100.dp
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeRefreshAndLoadLayout(
refreshingState: Boolean,
loadState: Boolean,
onRefresh: () -> Unit,
onLoad: () -> Unit,
content: @Composable () -> Unit
) {
val refreshDistance = with(LocalDensity.current) { RefreshDistance.toPx() }
val loadDistance = with(LocalDensity.current) { LoadDistance.toPx() }
val state = rememberSwipeableState(refreshingState) { newValue ->
// compare both copies of the swipe state before calling onRefresh(). This is a workaround.
if (newValue && !refreshingState) onRefresh()
true
}
val loadRefreshState = rememberSwipeableState(loadState) { newValue ->
// compare both copies of the swipe state before calling onRefresh(). This is a workaround.
if (newValue && !loadState) onLoad()
true
}
Box(
modifier = Modifier
.nestedScroll(state.PreUpPostDownNestedScrollConnection)
.swipeable(
state = state,
anchors = mapOf(
-refreshDistance to false,
refreshDistance to true,
),
thresholds = { _, _ -> FractionalThreshold(0.5f) },
orientation = Orientation.Vertical
)
.nestedScroll(loadRefreshState.LoadPreUpPostDownNestedScrollConnection)
.swipeable(
state = loadRefreshState,
anchors = mapOf(
loadDistance to false,
-loadDistance to true,
),
thresholds = { _, _ -> FractionalThreshold(0.5f) },
orientation = Orientation.Vertical
)
.fillMaxSize()
) {
content()
Box(
Modifier
.align(Alignment.TopCenter)
.offset { IntOffset(0, state.offset.value.roundToInt()) }
) {
if (state.offset.value != -refreshDistance) {
Surface(elevation = 10.dp, shape = CircleShape) {
CircularProgressIndicator(
modifier = Modifier
.size(36.dp)
.padding(4.dp)
)
}
}
}
Box(
Modifier
.align(Alignment.BottomCenter)
.offset { IntOffset(0, loadRefreshState.offset.value.roundToInt()) }
) {
if (loadRefreshState.offset.value != loadDistance) {
Surface(elevation = 10.dp, shape = CircleShape) {
CircularProgressIndicator(
modifier = Modifier
.size(36.dp)
.padding(4.dp)
)
}
}
}
// TODO (https://issuetracker.google.com/issues/164113834): This state->event trampoline is a
// workaround for a bug in the SwipableState API. Currently, state.value is a duplicated
// source of truth of refreshingState.
LaunchedEffect(refreshingState) { state.animateTo(refreshingState) }
LaunchedEffect(loadState) { loadRefreshState.animateTo(loadState) }
}
}
/**
* Temporary workaround for nested scrolling behavior. There is no default implementation for
* pull to refresh yet, this nested scroll connection mimics the behavior.
*/
@ExperimentalMaterialApi
private val <T> SwipeableState<T>.PreUpPostDownNestedScrollConnection: NestedScrollConnection
get() = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
return if (delta < 0 && source == NestedScrollSource.Drag) {
Log.e(TAG, "PreUpPost onPreScroll111: delta:$delta")
performDrag(delta).toOffset()
} else {
Log.e(TAG, "PreUpPost onPreScroll222: Offset.Zero:${Offset.Zero}")
Offset.Zero
}
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
return if (source == NestedScrollSource.Drag) {
Log.e(TAG, "PreUpPost onPostScroll: available.toFloat():${available.toFloat()}")
performDrag(available.toFloat()).toOffset()
} else {
Log.e(TAG, "PreUpPost onPostScroll: Offset.Zero:${Offset.Zero}")
Offset.Zero
}
}
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = Offset(available.x, available.y).toFloat()
return if (toFling < 0) {
Log.e(TAG, "PreUpPost onPreFling: available$available")
performFling(velocity = toFling)
// since we go to the anchor with tween settling, consume all for the best UX
// available
Velocity.Zero
} else {
Log.e(TAG, "PreUpPost onPreFling: Offset.Zero:${Offset.Zero}")
Velocity.Zero
}
}
override suspend fun onPostFling(
consumed: Velocity,
available: Velocity
): Velocity {
Log.e(TAG, "PreUpPost onPostFling: consumed:$consumed available:$available")
performFling(velocity = Offset(available.x, available.y).toFloat())
return Velocity.Zero
}
private fun Float.toOffset(): Offset = Offset(0f, this)
private fun Offset.toFloat(): Float = this.y
}
/**
* Temporary workaround for nested scrolling behavior. There is no default implementation for
* pull to refresh yet, this nested scroll connection mimics the behavior.
*/
@ExperimentalMaterialApi
private val <T> SwipeableState<T>.LoadPreUpPostDownNestedScrollConnection: NestedScrollConnection
get() = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
return if (delta > 0 && source == NestedScrollSource.Drag) {
Log.e(TAG, "LoadPreUp onPreScroll: delta:$delta")
performDrag(delta).toOffset()
} else {
Log.e(TAG, "LoadPreUp onPreScroll222: Offset.Zero:${Offset.Zero}")
Offset.Zero
}
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
return if (source == NestedScrollSource.Drag) {
Log.e(TAG, "LoadPreUp onPostScroll: available.toFloat():${available.toFloat()}")
performDrag(available.toFloat()).toOffset()
} else {
Log.e(TAG, "LoadPreUp onPostScroll: Offset.Zero:${Offset.Zero}")
Offset.Zero
}
}
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = Offset(available.x, available.y).toFloat()
return if (toFling > 0) {
Log.e(TAG, "LoadPreUp onPreFling: available$available")
performFling(velocity = toFling)
// since we go to the anchor with tween settling, consume all for the best UX
// available
Velocity.Zero
} else {
Log.e(TAG, "LoadPreUp onPreFling: Offset.Zero:${Offset.Zero}")
Velocity.Zero
}
}
override suspend fun onPostFling(
consumed: Velocity,
available: Velocity
): Velocity {
Log.e(TAG, "LoadPreUp onPostFling: consumed:$consumed available:$available")
performFling(velocity = Offset(available.x, available.y).toFloat())
return Velocity.Zero
}
private fun Float.toOffset(): Offset = Offset(0f, this)
private fun Offset.toFloat(): Float = this.y
}
@@ -0,0 +1,76 @@
package me.wcy.wanandroid.compose.widget
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.wcy.wanandroid.compose.R
import me.wcy.wanandroid.compose.theme.Colors
/**
* Created by wcy on 2021/3/31.
*/
@Composable
fun TitleBar(
title: String,
onBack: (() -> Unit)? = null,
@DrawableRes icon: Int? = null,
onIconClick: (() -> Unit)? = null,
) {
Row(
Modifier
.fillMaxWidth()
.height(48.dp)
.background(Colors.titleBar)
) {
if (onBack != null) {
Icon(
modifier = Modifier
.align(alignment = Alignment.CenterVertically)
.clickable {
onBack.invoke()
}
.size(48.dp)
.padding(14.dp),
painter = painterResource(id = R.drawable.ic_back),
contentDescription = "返回",
tint = Colors.text_h1
)
}
Text(
text = title,
modifier = Modifier
.align(alignment = Alignment.CenterVertically)
.padding(start = 16.dp, end = 16.dp)
.weight(1f),
color = Colors.text_h1,
fontSize = 17.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (icon != null) {
Icon(
modifier = Modifier
.align(alignment = Alignment.CenterVertically)
.clickable {
onIconClick?.invoke()
}
.size(48.dp)
.padding(14.dp),
painter = painterResource(id = icon),
contentDescription = "",
tint = Colors.text_h1
)
}
}
}
@@ -0,0 +1,31 @@
package me.wcy.wanandroid.compose.widget
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import me.wcy.wanandroid.compose.WanApplication
@SuppressLint("StaticFieldLeak")
object Toaster {
private val context = WanApplication.context
private val handler = Handler(Looper.getMainLooper())
fun show(resId: Int) {
show(context.getString(resId))
}
fun show(text: CharSequence?) {
if (text == null) {
return
}
val runnable = Runnable {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
}
if (Looper.myLooper() == Looper.getMainLooper()) {
runnable.run()
} else {
handler.post(runnable)
}
}
}
@@ -0,0 +1,5 @@
<?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" />
</adaptive-icon>
@@ -0,0 +1,5 @@
<?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" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#000000"
android:pathData="M761.056 532.128c0.512-0.992 1.344-1.824 1.792-2.848 8.8-18.304 5.92-40.704-9.664-55.424L399.936 139.744c-19.264-18.208-49.632-17.344-67.872 1.888-18.208 19.264-17.376 49.632 1.888 67.872l316.96 299.84-315.712 304.288c-19.072 18.4-19.648 48.768-1.248 67.872 9.408 9.792 21.984 14.688 34.56 14.688 12 0 24-4.48 33.312-13.44l350.048-337.376c0.672-0.672 0.928-1.6 1.6-2.304 0.512-0.48 1.056-0.832 1.568-1.344C757.76 538.88 759.2 535.392 761.056 532.128z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#000000"
android:pathData="M671.968 912c-12.288 0-24.576-4.672-33.952-14.048L286.048 545.984c-18.752-18.72-18.752-49.12 0-67.872l351.968-352c18.752-18.752 49.12-18.752 67.872 0 18.752 18.72 18.752 49.12 0 67.872l-318.016 318.048 318.016 318.016c18.752 18.752 18.752 49.12 0 67.872C696.544 907.328 684.256 912 671.968 912z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#000000"
android:pathData="M557.312 513.248l265.28-263.904c12.544-12.48 12.608-32.704 0.128-45.248-12.512-12.576-32.704-12.608-45.248-0.128l-265.344 263.936-263.04-263.84C236.64 191.584 216.384 191.52 203.84 204 191.328 216.48 191.296 236.736 203.776 249.28l262.976 263.776L201.6 776.8c-12.544 12.48-12.608 32.704-0.128 45.248 6.24 6.272 14.464 9.44 22.688 9.44 8.16 0 16.32-3.104 22.56-9.312l265.216-263.808 265.44 266.24c6.24 6.272 14.432 9.408 22.656 9.408 8.192 0 16.352-3.136 22.592-9.344 12.512-12.48 12.544-32.704 0.064-45.248L557.312 513.248z" />
</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,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#000000"
android:pathData="M512 928c-28.928 0-57.92-12.672-86.624-41.376L106.272 564C68.064 516.352 32 471.328 32 384c0-141.152 114.848-256 256-256 53.088 0 104 16.096 147.296 46.592 14.432 10.176 17.92 30.144 7.712 44.608-10.176 14.432-30.08 17.92-44.608 7.712C366.016 204.064 327.808 192 288 192c-105.888 0-192 86.112-192 192 0 61.408 20.288 90.112 59.168 138.688l315.584 318.816C486.72 857.472 499.616 863.808 512 864c12.704 0.192 24.928-6.176 41.376-22.624l316.672-319.904C896.064 493.28 928 445.696 928 384c0-105.888-86.112-192-192-192-48.064 0-94.08 17.856-129.536 50.272l-134.08 134.112c-12.512 12.512-32.736 12.512-45.248 0s-12.512-32.736 0-45.248l135.104-135.136C610.56 151.808 671.904 128 736 128c141.152 0 256 114.848 256 256 0 82.368-41.152 144.288-75.68 181.696l-317.568 320.8C569.952 915.328 540.96 928 512 928z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#000000"
android:pathData="M736 128c-65.952 0-128.576 25.024-176.384 70.464-4.576 4.32-28.672 28.736-47.328 47.68L464.96 199.04C417.12 153.216 354.272 128 288 128c-141.152 0-256 114.848-256 256 0 82.432 41.184 144.288 76.48 182.496l316.896 320.128C450.464 911.68 478.304 928 512 928c33.696 0 61.568-16.32 86.752-41.504l316.736-320 2.208-2.464C955.904 516.384 992 471.392 992 384 992 242.848 877.152 128 736 128z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#000000"
android:pathData="M953.504 908.256l-152.608-163.296c61.856-74.496 95.872-167.36 95.872-265.12 0-229.344-186.624-415.968-416.032-415.968-229.344 0-415.968 186.592-415.968 415.968s186.624 415.968 416 415.968c60.096-0.032 118.048-12.576 172.224-37.248 16.096-7.328 23.2-26.304 15.872-42.368-7.328-16.128-26.4-23.264-42.368-15.872-45.856 20.864-94.88 31.456-145.76 31.488-194.08 0-351.968-157.888-351.968-351.968 0-194.048 157.888-351.968 351.968-351.968 194.112 0 352.032 157.888 352.032 351.968 0 91.36-34.848 177.92-98.08 243.744-12.256 12.736-11.84 32.992 0.864 45.248 0.96 0.928 2.208 1.28 3.296 2.08 0.864 1.28 1.312 2.752 2.4 3.904l165.504 177.088c6.272 6.752 14.816 10.144 23.36 10.144 7.84 0 15.68-2.848 21.856-8.64C964.864 941.408 965.568 921.152 953.504 908.256z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#000000"
android:pathData="M768.704 703.616c-35.648 0-67.904 14.72-91.136 38.304l-309.152-171.712c9.056-17.568 14.688-37.184 14.688-58.272 0-12.576-2.368-24.48-5.76-35.936l304.608-189.152c22.688 20.416 52.384 33.184 85.216 33.184 70.592 0 128-57.408 128-128s-57.408-128-128-128-128 57.408-128 128c0 14.56 2.976 28.352 7.456 41.408l-301.824 187.392c-23.136-22.784-54.784-36.928-89.728-36.928-70.592 0-128 57.408-128 128 0 70.592 57.408 128 128 128 25.664 0 49.504-7.744 69.568-20.8l321.216 178.4c-3.04 10.944-5.184 22.208-5.184 34.08 0 70.592 57.408 128 128 128s128-57.408 128-128S839.328 703.616 768.704 703.616zM767.2 128.032c35.296 0 64 28.704 64 64s-28.704 64-64 64-64-28.704-64-64S731.904 128.032 767.2 128.032zM191.136 511.936c0-35.296 28.704-64 64-64s64 28.704 64 64c0 35.296-28.704 64-64 64S191.136 547.232 191.136 511.936zM768.704 895.616c-35.296 0-64-28.704-64-64s28.704-64 64-64 64 28.704 64 64S804 895.616 768.704 895.616z" />
</vector>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#000000"
android:pathData="M544 288c0-52.928-43.072-96-96-96s-96 43.072-96 96 43.072 96 96 96S544 340.928 544 288zM416 288c0-17.632 14.368-32 32-32s32 14.368 32 32-14.368 32-32 32S416 305.632 416 288z" />
<path
android:fillColor="#000000"
android:pathData="M304 448C259.904 448 224 483.904 224 528 224 572.128 259.904 608 304 608s80-35.872 80-80C384 483.904 348.096 448 304 448zM304 544C295.168 544 288 536.832 288 528S295.168 512 304 512s16 7.168 16 16S312.832 544 304 544z" />
<path
android:fillColor="#000000"
android:pathData="M640 736m-64 0a2 2 0 1 0 128 0 2 2 0 1 0-128 0Z" />
<path
android:fillColor="#000000"
android:pathData="M887.84 268.672c39.744-60.672 64.256-113.184 64.224-148.96 0-22.976-9.312-36.928-17.12-44.576-38.08-37.376-110.016-3.2-160.768 25.664-15.328 8.736-20.704 28.288-11.968 43.648 8.736 15.36 28.32 20.704 43.648 11.968 49.408-28.16 73.248-33.088 82.496-33.696-1.504 41.376-95.328 202.432-330.208 433.504-226.368 222.72-398.656 324.672-442.24 325.376 0.96-11.36 8.768-44.448 58.528-118.688 1.312-1.888 2.4-3.968 3.232-6.144l2.304-5.92c0.256-0.64 0.288-1.344 0.512-2.016 8.8-10.24 10.816-25.088 3.392-37.248C147.328 651.616 128 582.592 128 512 128 300.256 300.256 128 512 128c59.296 0 116.096 13.12 168.864 39.04 15.936 7.776 35.04 1.184 42.816-14.656 7.776-15.84 1.248-35.008-14.624-42.816C647.424 79.328 581.12 64 512 64 264.96 64 64 264.96 64 512c0 76.064 19.488 150.496 56.064 216.64-0.192 0.576-0.64 1.056-0.8 1.632C51.168 832.608 35.2 895.68 68.928 928.768c11.008 10.784 25.632 15.584 43.072 15.584 39.328 0 92.864-24.928 150.08-60.896C336.096 933.408 421.952 960 512 960c247.04 0 448-200.96 448-448C960 424.864 934.912 341.248 887.84 268.672zM512 896c-68.288 0-133.728-17.888-191.84-51.552 128.288-92.256 258.432-218.56 282.848-242.624 89.152-87.712 178.56-187.04 244.64-275.968C879.2 382.592 896 446.112 896 512 896 723.744 723.744 896 512 896z" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#000000"
android:pathData="M928.768 87.232c-0.032-7.456-1.216-13.696-2.848-19.328-31.328-8.288-72.128 1.248-106.624 17.984-7.072 3.424-23.84 13.632-48.672 28.736-10.272 6.272-19.328 11.776-23.328 14.048-0.384 0.224-0.608 0.64-0.96 0.864C678.56 88.256 599.296 64 514.272 64c-247.04 0-448 200.96-448 448 0 82.816 22.976 160.192 62.304 226.784-46.464 71.616-69.504 123.328-63.488 158.368 1.248 7.168 13.6 8.32 20.384 8.32 138.432 0 454.848-303.776 494.208-342.496C753.504 392 928.864 182.976 928.768 87.232zM306.272 610.688c-44.096 0-80-35.872-80-80 0-44.096 35.904-80 80-80s80 35.904 80 80C386.272 574.816 350.368 610.688 306.272 610.688zM447.072 386.688c-52.928 0-96-43.072-96-96s43.072-96 96-96 96 43.072 96 96S500 386.688 447.072 386.688z" />
<path
android:fillColor="#000000"
android:pathData="M895.904 278.56c-67.104 99.648-172.608 219.296-277.856 322.784-28.64 28.192-202.272 196.448-348.096 286.112 0.128-0.096 0.224-0.16 0.352-0.224C340.576 933.088 424.288 960 514.272 960c247.04 0 448-200.96 448-448C962.24 426.4 937.728 346.656 895.904 278.56zM640.032 800c-35.296 0-64-28.704-64-64s28.704-64 64-64 64 28.704 64 64S675.328 800 640.032 800z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#666666"
android:pathData="M96 480c-9.6 0-19.2-3.2-25.6-12.8-12.8-12.8-9.6-35.2 3.2-44.8l377.6-310.4c35.2-25.6 86.4-25.6 118.4 0l377.6 307.2c12.8 9.6 16 32 3.2 44.8-12.8 12.8-32 16-44.8 3.2L531.2 166.4c-9.6-6.4-28.8-6.4-38.4 0L115.2 473.6c-6.4 6.4-12.8 6.4-19.2 6.4zM816 928H608c-19.2 0-32-12.8-32-32v-150.4c0-22.4-38.4-44.8-67.2-44.8-28.8 0-64 19.2-64 44.8V896c0 19.2-12.8 32-32 32H211.2C163.2 928 128 892.8 128 848V544c0-19.2 12.8-32 32-32s32 12.8 32 32v304c0 9.6 6.4 16 19.2 16H384v-118.4c0-64 67.2-108.8 128-108.8s131.2 44.8 131.2 108.8V864h176c9.6 0 16 0 16-19.2V544c0-19.2 12.8-32 32-32s32 12.8 32 32v304C896 896 864 928 816 928z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#666666"
android:pathData="M947.2 422.4L572.8 115.2c-32-25.6-86.4-25.6-118.4 0L76.8 425.6c-12.8 6.4-16 22.4-9.6 35.2 3.2 12.8 16 19.2 28.8 19.2h32v364.8C128 892.8 163.2 928 211.2 928H416c19.2 0 32-12.8 32-32v-147.2c0-22.4 35.2-44.8 64-44.8 28.8 0 67.2 22.4 67.2 44.8V896c0 19.2 12.8 32 32 32h208c48 0 80-32 80-83.2V480h32c12.8 0 25.6-9.6 28.8-22.4 3.2-12.8 0-25.6-12.8-35.2z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#666666"
android:pathData="M800 384c0-160-128-288-288-288s-288 128-288 288c0 108.8 57.6 201.6 147.2 249.6-121.6 48-214.4 153.6-240 288-3.2 16 6.4 35.2 25.6 38.4h6.4c16 0 28.8-9.6 32-25.6 28.8-150.4 160-259.2 313.6-262.4h6.4c156.8 0 284.8-128 284.8-288zM288 384c0-124.8 99.2-224 224-224s224 99.2 224 224c0 121.6-99.2 220.8-220.8 224H505.6C384 604.8 288 505.6 288 384zM723.2 675.2c-16-9.6-35.2-6.4-44.8 9.6-9.6 16-6.4 35.2 9.6 44.8 73.6 51.2 124.8 121.6 140.8 204.8 3.2 16 16 25.6 32 25.6h6.4c16-3.2 28.8-19.2 25.6-38.4-19.2-99.2-80-185.6-169.6-246.4z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1024dp"
android:height="1024dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#666666"
android:pathData="M649.6 633.6c86.4-48 147.2-144 147.2-249.6 0-160-128-288-288-288s-288 128-288 288c0 108.8 57.6 201.6 147.2 249.6-121.6 48-214.4 153.6-240 288-3.2 9.6 0 19.2 6.4 25.6 3.2 9.6 12.8 12.8 22.4 12.8h704c9.6 0 19.2-3.2 25.6-12.8 6.4-6.4 9.6-16 6.4-25.6-25.6-134.4-121.6-240-243.2-288z" />
</vector>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1194dp"
android:height="1024dp"
android:viewportWidth="1194"
android:viewportHeight="1024">
<path
android:fillColor="#515151"
android:pathData="M728.064 535.296a35.498667 35.498667 0 1 1-70.912 0 35.498667 35.498667 0 0 1 70.912 0M974.08 535.296a35.498667 35.498667 0 1 1-70.997333 0 35.498667 35.498667 0 0 1 70.997333 0" />
<path
android:fillColor="#515151"
android:pathData="M902.144 930.133333l-6.656 1.450667a594.176 594.176 0 0 1-18.602667 3.669333c-26.453333 4.778667-44.629333 6.826667-64 6.144-167.850667-5.546667-298.666667-87.893333-351.061333-218.709333a446.464 446.464 0 0 1-6.826667-19.114667l-1.962666-6.058666a199.68 199.68 0 0 1-3.84-13.653334 338.858667 338.858667 0 0 1-9.216-75.690666c0-171.008 157.013333-305.92 354.304-314.709334l8.874666-0.682666c7.850667-0.597333 12.970667-0.853333 18.773334-0.853334H836.693333l9.301334 0.170667c188.16 14.677333 340.394667 156.672 340.394666 323.925333 0 78.336-38.826667 155.733333-109.653333 222.293334a303.530667 303.530667 0 0 1-7.082667 6.4l9.984 71.082666a49.152 49.152 0 0 1-69.12 58.453334l-98.816-46.421334a582.485333 582.485333 0 0 1-9.472 2.304z m220.16-314.026666c0-131.754667-124.586667-247.978667-279.125333-260.096H821.930667c-3.754667 0-7.68 0.085333-13.994667 0.597333l-10.666667 0.768C631.466667 364.8 503.978667 474.453333 503.978667 608.256c0 18.773333 2.730667 40.874667 7.509333 60.842667 0.597333 2.474667 1.450667 5.632 2.56 9.216l1.706667 4.949333c2.133333 6.570667 4.266667 12.629333 5.632 15.957333 42.325333 105.728 149.930667 173.397333 293.717333 178.176 13.738667 0.512 28.16-1.109333 50.346667-5.12a531.626667 531.626667 0 0 0 16.64-3.242666l5.632-1.28c4.693333-1.109333 8.448-1.962667 19.370666-4.778667a32 32 0 0 1 21.418667 2.133333l85.248 40.106667-9.984-69.290667a32 32 0 0 1 13.909333-31.232c1.194667-0.853333 8.618667-6.912 15.189334-13.056 58.709333-55.210667 89.429333-116.394667 89.429333-175.616z m-1109.333333-237.909334C12.970667 171.52 206.677333 5.376 442.709333 5.376c208.384 0 394.24 130.304 431.872 306.090667 1.28 5.973333 1.28 11.264 0.512 16.896a32 32 0 0 1-63.658666-5.973334C779.178667 179.2 621.482667 69.376 442.709333 69.376 240.042667 69.376 76.970667 209.152 76.970667 378.282667c0 89.941333 46.165333 171.52 136.021333 237.568a32 32 0 0 1 11.434667 35.84l-23.296 69.973333 104.96-48.896a32 32 0 0 1 22.442666-1.706667l5.461334 1.706667c54.357333 11.093333 76.288 14.506667 108.714666 14.506667a200.789333 200.789333 0 0 0 32.256-4.010667c-0.512 0.170667-1.536 0.512-2.730666 1.536a33.109333 33.109333 0 0 1 39.253333 1.109333 32 32 0 0 1 5.376 44.970667c-11.776 14.848-47.36 20.309333-74.24 20.309333-37.12 0-62.037333-3.584-119.893333-15.530666L198.570667 793.6a49.493333 49.493333 0 0 1-68.949334-59.733333l26.88-80.64C62.976 577.706667 12.970667 482.986667 12.970667 378.197333z" />
<path
android:fillColor="#515151"
android:pathData="M344.576 254.378667a44.373333 44.373333 0 1 1-88.746667 0.085333 44.373333 44.373333 0 0 1 88.746667 0M636.586667 254.378667a44.373333 44.373333 0 1 1-88.746667 0.085333 44.373333 44.373333 0 0 1 88.746667 0" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1193dp"
android:height="1024dp"
android:viewportWidth="1193"
android:viewportHeight="1024">
<path
android:fillColor="#000000"
android:pathData="M806.052074 309.710659c13.950931 0 27.901861 0.930062 40.92273 2.790186C809.772322 133.928934 629.340286 0.930062 421.936451 0.930062 190.351003 0.930062 1.548409 167.411167 1.548409 378.53525c0 120.908065 62.314157 221.354765 167.411167 299.479977l-41.852791 132.998872L274.986649 734.749011c52.083474 11.160744 94.866328 22.321489 146.949802 22.321489 13.020869 0 26.971799 0 39.062606-1.860124a353.423575 353.423575 0 0 1 344.122955-444.569655zM580.046998 189.732656c31.622109 0 53.013536 22.321489 53.013536 55.803723 0 32.552171-21.391427 55.803722-53.013536 55.803722-31.622109 0-63.244219-23.251551-63.244219-55.803722 0-33.482233 31.622109-55.803722 63.244219-55.803723z m-294.829667 111.607445c-31.622109 0-63.244219-23.251551-63.244218-55.803722 0-33.482233 31.622109-55.803722 63.244218-55.803723s53.013536 22.321489 53.013537 55.803723c0 32.552171-21.391427 55.803722-53.013537 55.803722z" />
<path
android:fillColor="#000000"
android:pathData="M1191.097758 655.693738c0-176.711788-169.271291-321.801466-358.073885-321.801466C633.060534 333.892272 474.949988 478.051888 474.949988 655.693738s158.110547 321.801466 358.073885 321.801466c41.852792 0 83.705584-11.160744 126.488437-22.321489l115.327693 66.964467-31.622109-111.607445c83.705584-65.104343 146.949802-154.390299 146.949802-254.836999z m-474.33164-55.803722c-20.461365 0-41.852792-21.391427-41.852792-43.712916 0-22.321489 21.391427-44.642978 41.852792-44.642978 31.622109 0 53.013536 22.321489 53.013536 44.642978s-21.391427 44.642978-53.013536 44.642978z m232.51551 0c-21.391427 0-42.782854-21.391427-42.782854-43.712916 0-22.321489 21.391427-44.642978 41.852792-44.642978 31.622109 0 53.013536 22.321489 53.013536 44.642978s-21.391427 44.642978-53.013536 44.642978z" />
</vector>
@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Wanandroidcompose" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
@@ -0,0 +1,13 @@
<?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>
<color name="main">#FF43A047</color>
<color name="title_bar">#FFFAFAFA</color>
<color name="bottom_bar">#FFFAFAFA</color>
</resources>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">wanandroid-compose</string>
</resources>
@@ -0,0 +1,27 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Wanandroidcompose" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/main</item>
<item name="colorPrimaryVariant">@color/main</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/main</item>
<item name="colorSecondaryVariant">@color/main</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">@color/title_bar</item>
<!-- Customize your theme here. -->
<item name="android:navigationBarColor">@color/bottom_bar</item>
<item name="android:windowLightStatusBar" tools:ignore="NewApi">true</item>
</style>
<style name="Theme.Wanandroidcompose.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.Wanandroidcompose.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="Theme.Wanandroidcompose.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>