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
+10
View File
@@ -0,0 +1,10 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+23
View File
@@ -0,0 +1,23 @@
# wanandroid-compose
Jetpack Compose 版本的「玩 Android」
## 环境
- AndroidStudio: Arctic Fox | 2020.3.1
## 功能
- 登录、注册
- 等级、排名、积分、收藏
- 首页、搜索
- 广场
- 公众号
## 下载体验
![](https://raw.githubusercontent.com/wangchenyan/wanandroid-compose/master/art/qrcode.png)
https://github.com/wangchenyan/wanandroid-compose/releases
## 截图
![](https://raw.githubusercontent.com/wangchenyan/wanandroid-compose/master/art/screenshot01.jpg)
![](https://raw.githubusercontent.com/wangchenyan/wanandroid-compose/master/art/screenshot02.jpg)
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@@ -0,0 +1,20 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
val compose_version by extra("1.0.1")
val kotlin_version by extra("1.5.21")
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.0.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${rootProject.extra["kotlin_version"]}")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle.kts files
}
}
tasks.register("clean", Delete::class) {
delete(rootProject.buildDir)
}
@@ -0,0 +1,21 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
Binary file not shown.
@@ -0,0 +1,6 @@
#Tue Mar 30 20:56:37 CST 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
+84
View File
@@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -0,0 +1,12 @@
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
jcenter() // Warning: this repository is going to shut down soon
maven { setUrl("https://jitpack.io") }
}
}
rootProject.name = "wanandroid-compose"
include(":app")