a
@@ -0,0 +1,10 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
@@ -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.
|
||||
@@ -0,0 +1,23 @@
|
||||
# wanandroid-compose
|
||||
|
||||
Jetpack Compose 版本的「玩 Android」
|
||||
|
||||
## 环境
|
||||
- AndroidStudio: Arctic Fox | 2020.3.1
|
||||
|
||||
## 功能
|
||||
- 登录、注册
|
||||
- 等级、排名、积分、收藏
|
||||
- 首页、搜索
|
||||
- 广场
|
||||
- 公众号
|
||||
|
||||
## 下载体验
|
||||

|
||||
|
||||
https://github.com/wangchenyan/wanandroid-compose/releases
|
||||
|
||||
## 截图
|
||||

|
||||
|
||||

|
||||
@@ -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")
|
||||
}
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
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>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,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>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
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
|
||||
@@ -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
|
||||
@@ -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" "$@"
|
||||
@@ -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")
|
||||
|
||||