a
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
*.iml
|
||||
.gradle
|
||||
.idea
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
@@ -0,0 +1,219 @@
|
||||
## 前言
|
||||
今年七月底,`Google` 正式发布了 `Jetpack Compose` 的 `1.0` 稳定版本,这说明`Google`认为`Compose`已经可以用于生产环境了。相信`Compose`的广泛应用就在不远的将来,现在应该是学习`Compose`的一个比较好的时机
|
||||
在了解了`Compose`的基本知识与原理之后,通过一个完整的项目继续学习`Compose`应该是一个比较好的方式。本文主要基于`Compose`,`MVI`架构,单`Activity`架构等,快速实现一个`wanAndroid`客户端,如果对您有所帮助可以点个`Star`: [wanAndroid-compose](https://github.com/shenzhen2017/wanandroid-compose)
|
||||
|
||||
## 效果图
|
||||
首先看下效果图
|
||||
|
||||
|  |  |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|  |  |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|  |  |
|
||||
|
||||
## 主要实现介绍
|
||||
各个页面的具体实现可以查看源码,这里主要介绍一些主要的实现与原理
|
||||
|
||||
### 使用`MVI`架构
|
||||
`MVI` 与 `MVVM` 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,架构图如下所示
|
||||

|
||||
其主要分为以下几部分
|
||||
1. `Model`: 与`MVVM`中的`Model`不同的是,`MVI`的`Model`主要指`UI`状态(`State`)。例如页面加载状态、控件位置等都是一种`UI`状态
|
||||
2. `View`: 与其他`MVX`中的`View`一致,可能是一个`Activity`或者任意`UI`承载单元。`MVI`中的`View`通过订阅`Model`的变化实现界面刷新
|
||||
3. `Intent`: 此`Intent`不是`Activity`的`Intent`,用户的任何操作都被包装成`Intent`后发送给`Model`层进行数据请求
|
||||
|
||||
例如登录页面的`Model`与`Intent`定义如下
|
||||
```kotlin
|
||||
/**
|
||||
* 页面所有状态
|
||||
/
|
||||
data class LoginViewState(
|
||||
val account: String = "",
|
||||
val password: String = "",
|
||||
val isLogged: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* 一次性事件
|
||||
*/
|
||||
sealed class LoginViewEvent {
|
||||
object PopBack : LoginViewEvent()
|
||||
data class ErrorMessage(val message: String) : LoginViewEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面Intent,即用户的操作
|
||||
/
|
||||
sealed class LoginViewAction {
|
||||
object Login : LoginViewAction()
|
||||
object ClearAccount : LoginViewAction()
|
||||
object ClearPassword : LoginViewAction()
|
||||
data class UpdateAccount(val account: String) : LoginViewAction()
|
||||
data class UpdatePassword(val password: String) : LoginViewAction()
|
||||
}
|
||||
```
|
||||
如上所示
|
||||
1. 通过`ViewState`定义页面所有状态
|
||||
2. `ViewEvent`定义一次性事件如`Toast`,页面关闭事件等
|
||||
3. 通过`ViewAction`定义所有用户操作
|
||||
|
||||
`MVI`架构与`MVVM`架构的主要区别在于:
|
||||
1. `MVVM`并没有约束`View`层与`ViewModel`的交互方式,具体来说就是`View`层可以随意调用`ViewModel`中的方法,而`MVI`架构下`ViewModel`的实现对`View`层屏蔽,只能通过发送`Intent`来驱动事件。
|
||||
2. `MVVM` 的 `ViewModle` 中分散定义了多个 `State` ,`MVI` 使用 `ViewState` 对 `State` 集中管理,只需要订阅一个 `ViewState` 便可获取页面的所有状态,相对 `MVVM` 减少了不少模板代码
|
||||
|
||||
`Compose` 的声明式`UI`思想来自 `React`,理论上同样来自 `Redux` 思想的 `MVI` 应该是 `Compose` 的最佳伴侣
|
||||
但是`MVI`也只是在`MVVM`的基础上做了一定的改良,`MVVM` 也可以很好地配合 `Compose` 使用,各位可根据自己的需要选择合适的架构
|
||||
|
||||
关于`Compose`的架构选择可参考:[Jetpack Compose 架构如何选? MVP, MVVM, MVI](https://juejin.cn/post/6969382803112722446)
|
||||
|
||||
### 单`Activity`架构
|
||||
早在`View`时代,就有不少推荐单`Activity`+多`Fragment`架构的文章,`Google`也推出了`Jetpack Navigation`库来支持这种单`Activity`架构
|
||||
对于`Compose`来说,因为`Activity`与`Compose`是通过`AndroidComposeView`来中转的,`Activity`越多,就需要创建出越多的`AndroidComposeView`,对性能有一定影响
|
||||
而使用单`Activity`架构,所有变换页面跳转都在`Compose`内部完成,可能也是出于这个原因,目前`Google`的示例项目都是基于单`Activity`+`Navigation`+多`Compose`架构的
|
||||
|
||||
但是使用单`Activity`架构也需要解决一些问题
|
||||
1. 所有的`viewModel`都在一个`Activity`的`ViewModelStoreOwner`中,那么当一个页面销毁了,此页面用过的`viewModel`应该什么时候销毁呢?
|
||||
2. 有时候页面需要监听自己这个页面的`onResume`,`onPause`等生命周期,单`Activity`架构下如何监听生命周期呢?
|
||||
|
||||
我们下面就一起来看下如何解决单`Activity`架构下的这两个问题
|
||||
|
||||
### 页面`ViewModel`何时销毁?
|
||||
在`Compose`中一般可以通过以下两种方式获取`ViewModel`
|
||||
```kotlin
|
||||
//方式1
|
||||
@Composable
|
||||
fun LoginPage(
|
||||
loginViewModel: LoginViewModel = viewModel()
|
||||
) {
|
||||
//...
|
||||
}
|
||||
|
||||
//方式2
|
||||
@Composable
|
||||
fun LoginPage(
|
||||
loginViewModel: LoginViewModel = hiltViewModel()
|
||||
) {
|
||||
//...
|
||||
}
|
||||
```
|
||||
如上所示:
|
||||
1. 方式1将返回一个与`ViewModelStoreOwner`(一般是`Activity`或`Fragment`)绑定的`ViewModel`,如果不存在则创建,已存在则直接返回。很明显通过这种方式创建的`ViewModel`的生命周期将与`Activity`一致,在单`Activity`架构中将一直存在,不会释放。
|
||||
2. 方式2通过`Hilt`实现,可以在`Composable`中获取`NavGraph Scope`或`Destination Scope` 的 `ViewModel`,并自动依赖 `Hilt` 构建。`Destination Scope` 的 `ViewModel` 会跟随 `BackStack` 的弹出自动 `Clear` ,避免泄露。
|
||||
|
||||
总得来说,通过`hiltViewModel`与`Navigation`配合,是一个更好的选择
|
||||
|
||||
### `Compose`如何获取生命周期?
|
||||
为了在`Compose`中获取生命周期,我们需要先了解下[副作用](https://developer.android.google.cn/jetpack/compose/side-effects?hl=zh-cn)
|
||||
用一句话概括副作用:一个函数的执行过程中,除了返回函数值之外,对调用方还会带来其他附加影响,例如修改全局变量或修改参数等。
|
||||
|
||||
副作用必须在合适的时机执行,我们首先需要明确一下`Composable`的生命周期:
|
||||
1. `onActive(or onEnter)`:当`Composable`首次进入组件树时
|
||||
2. `onCommit(or onUpdate)`:`UI`随着`recomposition`发生更新时
|
||||
3. `onDispose(or onLeave)`:当`Composable`从组件树移除时
|
||||
|
||||

|
||||
|
||||
了解了`Compose`的生命周期后,我们可以发现,如果我们在`onActive`时监听`Activity`的生命周期,在`onDispose`时取消监听,不就可以实现在`Compose`中获取生命周期了吗?
|
||||
`DisposableEffect`可以帮助我们实现这个需求,`DisposableEffect`在其监听的`Key`发生变化,或`onDispose`时会执行
|
||||
我们还可以通过添加参数,让其仅在`onActive`与`onDispose`时执行:例如`DisposableEffect(true)`或`DisposableEffect(Unit)`
|
||||
|
||||
通过以下方式,就可以实现在`Compose`中监听页面生命周期
|
||||
```kotlin
|
||||
@Composable
|
||||
fun LoginPage(
|
||||
loginViewModel: LoginViewModel = hiltViewModel()
|
||||
) {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(key1 = Unit) {
|
||||
val observer = object : LifecycleObserver {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
fun onResume() {
|
||||
viewModel.dispatch(Action.Resume)
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
fun onPause() {
|
||||
viewModel.dispatch(Action.Pause)
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当然有时也不需要这么复杂,比如我们需要在进入或返回`ProfilePage`页面时刷新登录状态,并根据登录状态确认页面`UI`,就可以通过以下方式实现
|
||||
```kotlin
|
||||
@Composable
|
||||
fun ProfilePage(
|
||||
navCtrl: NavHostController,
|
||||
scaffoldState: ScaffoldState,
|
||||
viewModel: ProfileViewModel = hiltViewModel()
|
||||
) {
|
||||
//...
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
Log.i("debug", "onStart")
|
||||
viewModel.dispatch(ProfileViewAction.OnStart)
|
||||
onDispose {
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
如上所示,每当进入页面或返回该页面时,我们就可以刷新页面登录状态了
|
||||
|
||||
### `Compose`如何保存`LazyColumn`列表状态
|
||||
相信使用过`LazyColumn`的同学都碰到过下面的问题
|
||||
> 使用`Paging3`加载分页数据,并显示到页面`A`的`LazyColumn`上,向下滑动`LazyColumn`,然后`navigation.navigate`跳转到页面`B`,接着再`navigatUp`回到页面`A`,页面`A`的`LazyColumn`又回到了列表顶部
|
||||
|
||||
但是我们可以看到,`LazyListState`其实是通过`rememberLazyListState`做了持久化保存的,如下图所示
|
||||

|
||||
|
||||
既然做了持久化保存,那为什么返回时的位置还有问题呢?其实纯粹使用 `Paging` + `LazyColumn`,当页面切换时,会记录当前页面位置,但如果通过`item`加上`Header`或`Footer`就不行了
|
||||
这是因为`rememberLazyListState`会在列表中至少有一项时`restore`滚动位置,同时`Paging`是通过`Flow`获取数据的,当返回到页面重组时并不能马上获取到`Paging`数据,第一帧时`Paging`的`itemCount`为0
|
||||
但同时因为`LazyColumn`中已经有了一个`Header`,这时便会还原保存的位置,但因为这时`Paging`中的数据还为空,不能滚动到正确的位置,于是便又滚动到顶部了
|
||||
而当`LazyColumn`中没有`Header`时,列表中至少有一项时便是`Paging`数据成功填充的时候,这个时候还原的位置就是对的,所以没有问题
|
||||
|
||||
既然原因在于`LazyListState`没有在正确的时机被还原,那我们将`LazyListSate`保存在`ViewModel`中,并且在`Paging`中有数据时再还原`listState`,如下所示:
|
||||
```kotlin
|
||||
@HiltViewModel
|
||||
class SquareViewModel @Inject constructor(
|
||||
private var service: HttpService,
|
||||
) : ViewModel() {
|
||||
private val pager by lazy { simplePager { service.getSquareData(it) }.cachedIn(viewModelScope) }
|
||||
val listState: LazyListState = LazyListState()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SquarePage(
|
||||
navCtrl: NavHostController,
|
||||
scaffoldState: ScaffoldState,
|
||||
viewModel: SquareViewModel = hiltViewModel()
|
||||
) {
|
||||
val squareData = viewStates.pagingData.collectAsLazyPagingItems()
|
||||
// 当`Paging`有数据时,返回`ViewModel`中的`listState`
|
||||
val listState = if (squareData.itemCount > 0) viewStates.listState else LazyListState()
|
||||
|
||||
RefreshList(squareData, listState = listState) {
|
||||
itemsIndexed(squareData) { _, item ->
|
||||
//...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
总得来说,对于一般的页面,`rememberLazyListState`已经足够,但是对于有`Header`或`Footer`的`Paging`页面,需要一些特殊处理
|
||||
关于`LazyColumn`滚动丢失的问题,更详细的讨论可参考:[Scroll position of LazyColumn built with collectAsLazyPagingItems is lost when using Navigation
|
||||
](https://issuetracker.google.com/issues/177245496?pli=1)
|
||||
|
||||
## 总结
|
||||
### 项目地址
|
||||
[https://github.com/shenzhen2017/wanandroid-compose](https://github.com/shenzhen2017/wanandroid-compose)
|
||||
开源不易,如果项目对你有所帮助,欢迎点赞,`Star`,收藏~
|
||||
|
||||
### 参考资料
|
||||
[https://github.com/manqianzhuang/HamApp](https://github.com/manqianzhuang/HamApp)
|
||||
[https://github.com/linxiangcheer/PlayAndroid](https://github.com/linxiangcheer/PlayAndroid)
|
||||
[从零到一写一个完整的 Compose 版本的天气](https://juejin.cn/post/7030986229512404999)
|
||||
@@ -0,0 +1,16 @@
|
||||
*.iml
|
||||
.gradle
|
||||
.idea
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
@@ -0,0 +1,112 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id "kotlin-kapt"
|
||||
id "kotlin-parcelize"
|
||||
id "dagger.hilt.android.plugin"
|
||||
id "androidx.navigation.safeargs.kotlin"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 31
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.zj.wanandroid"
|
||||
minSdk 21
|
||||
targetSdk 31
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
useIR = true
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion compose_version
|
||||
kotlinCompilerVersion kotlin_version
|
||||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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:$compose_version"
|
||||
implementation "androidx.compose.material:material:$compose_version"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
||||
implementation 'androidx.activity:activity-compose:1.3.1'
|
||||
testImplementation 'junit:junit:4.+'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||
implementation "androidx.navigation:navigation-compose:2.4.0-beta02"
|
||||
|
||||
/** accompanist辅助插件 */
|
||||
implementation "com.google.accompanist:accompanist-insets:$accompanistVersion"
|
||||
implementation "com.google.accompanist:accompanist-coil:0.15.0"
|
||||
//系统ui控制器
|
||||
implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion"
|
||||
//glide
|
||||
implementation "com.google.accompanist:accompanist-glide:0.15.0"
|
||||
//viewPager
|
||||
implementation "com.google.accompanist:accompanist-pager:0.20.2"
|
||||
implementation "com.google.accompanist:accompanist-pager-indicators:$accompanistVersion"
|
||||
//下拉刷新
|
||||
implementation "com.google.accompanist:accompanist-swiperefresh:$accompanistVersion"
|
||||
//流式布局
|
||||
implementation "com.google.accompanist:accompanist-flowlayout:$accompanistVersion"
|
||||
//placeholder
|
||||
implementation "com.google.accompanist:accompanist-placeholder-material:$accompanistVersion"
|
||||
|
||||
//hilt inject framework
|
||||
implementation "com.google.dagger:hilt-android:$hiltVersion"
|
||||
kapt "com.google.dagger:hilt-compiler:$hiltVersion"
|
||||
implementation "androidx.hilt:hilt-navigation-compose:$hiltComposeVersion"
|
||||
implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hiltComposeVersion"
|
||||
kapt "androidx.hilt:hilt-compiler:$hiltCompilerVersion"
|
||||
|
||||
//http request
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
||||
implementation "com.google.code.gson:gson:$gsonVersion"
|
||||
|
||||
//数据保存,用于cookie持久化
|
||||
implementation "androidx.datastore:datastore-preferences:$datastoreVersion"
|
||||
implementation "androidx.datastore:datastore-core:$datastoreVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:1.5.31"
|
||||
|
||||
//paging分页库
|
||||
implementation "androidx.paging:paging-runtime:$pagingVersion"
|
||||
testImplementation "androidx.paging:paging-common:$pagingVersion"
|
||||
implementation "androidx.paging:paging-compose:$pagingComposeVersion"
|
||||
|
||||
//约束布局
|
||||
implementation "androidx.constraintlayout:constraintlayout-compose:$constraintComposeVersion"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.zj.wanandroid
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.zj.wanandroid", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.zj.wanandroid">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:name=".MyApp"
|
||||
android:theme="@style/Theme.Wanandroidcompose">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,23 @@
|
||||
package com.zj.wanandroid
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.zj.wanandroid.ui.page.common.HomeEntry
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
@ExperimentalFoundationApi
|
||||
@ExperimentalComposeUiApi
|
||||
@ExperimentalPagerApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
HomeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.zj.wanandroid
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
/**
|
||||
* 1. 所有使用 Hilt 的 App 必须包含 一个使用 @HiltAndroidApp 注解的 Application
|
||||
* 2. @HiltAndroidApp 将会触发 Hilt 代码的生成,包括用作应用程序依赖项容器的基类
|
||||
* 3. 生成的 Hilt 组件依附于 Application 的生命周期,它也是 App 的父组件,提供其他组件访问的依赖
|
||||
* 4. 在 Application 中设置好 @HiltAndroidApp 之后,就可以使用 Hilt 提供的组件了,
|
||||
* Hilt 提供的 @AndroidEntryPoint 注解用于提供 Android 类的依赖(Activity、Fragment、View、Service、BroadcastReceiver)等等
|
||||
* Application 使用 @HiltAndroidApp 注解
|
||||
*/
|
||||
@HiltAndroidApp
|
||||
class MyApp : Application() {
|
||||
companion object {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
lateinit var CONTEXT: Context
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
CONTEXT = this
|
||||
DataStoreUtils.init(this)
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.zj.wanandroid.common.di
|
||||
|
||||
import com.zj.wanandroid.data.http.ApiCall
|
||||
import com.zj.wanandroid.data.http.HttpService
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Singleton
|
||||
|
||||
//这里使用了SingletonComponent,因此 NetworkModule 绑定到 Application 的生命周期
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class NetworkModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideApiService(): HttpService = ApiCall.retrofit
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideOkHttp(): OkHttpClient = ApiCall.okHttp
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.zj.wanandroid.common.paging
|
||||
|
||||
import androidx.paging.PagingConfig
|
||||
|
||||
data class AppPagingConfig(
|
||||
val pageSize: Int = 20,
|
||||
val initialLoadSize: Int = 20,
|
||||
val prefetchDistance:Int = 1,
|
||||
val maxSize:Int = PagingConfig.MAX_SIZE_UNBOUNDED,
|
||||
val enablePlaceholders:Boolean = false
|
||||
)
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.zj.wanandroid.common.paging
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.*
|
||||
import com.zj.wanandroid.MyApp
|
||||
import com.zj.wanandroid.data.bean.BasicBean
|
||||
import com.zj.wanandroid.data.bean.ListWrapper
|
||||
import com.zj.wanandroid.data.http.HttpResult
|
||||
import com.zj.wanandroid.utils.NetCheckUtil
|
||||
import com.zj.wanandroid.utils.showToast
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
fun <T : Any> ViewModel.simplePager(
|
||||
config: AppPagingConfig = AppPagingConfig(),
|
||||
callAction: suspend (page: Int) -> BasicBean<ListWrapper<T>>
|
||||
): Flow<PagingData<T>> {
|
||||
return pager(config, 0) {
|
||||
val page = it.key ?: 0
|
||||
val response = try {
|
||||
HttpResult.Success(callAction.invoke(page))
|
||||
} catch (e: Exception) {
|
||||
if (NetCheckUtil.checkNet(MyApp.CONTEXT).not()) {
|
||||
showToast("没有网络,请重试")
|
||||
} else {
|
||||
showToast("请求失败,请重试")
|
||||
}
|
||||
HttpResult.Error(e)
|
||||
}
|
||||
when (response) {
|
||||
is HttpResult.Success -> {
|
||||
val data = response.result.data
|
||||
val hasNotNext = (data!!.datas.size < it.loadSize) && (data.over)
|
||||
PagingSource.LoadResult.Page(
|
||||
data = response.result.data!!.datas,
|
||||
prevKey = if (page - 1 > 0) page - 1 else null,
|
||||
nextKey = if (hasNotNext) null else page + 1
|
||||
)
|
||||
}
|
||||
is HttpResult.Error -> {
|
||||
PagingSource.LoadResult.Error(response.exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <K : Any, V : Any> ViewModel.pager(
|
||||
config: AppPagingConfig = AppPagingConfig(),
|
||||
initialKey: K? = null,
|
||||
loadData: suspend (PagingSource.LoadParams<K>) -> PagingSource.LoadResult<K, V>
|
||||
): Flow<PagingData<V>> {
|
||||
val baseConfig = PagingConfig(
|
||||
config.pageSize,
|
||||
initialLoadSize = config.initialLoadSize,
|
||||
prefetchDistance = config.prefetchDistance,
|
||||
maxSize = config.maxSize,
|
||||
enablePlaceholders = config.enablePlaceholders
|
||||
)
|
||||
return Pager(
|
||||
config = baseConfig,
|
||||
initialKey = initialKey
|
||||
) {
|
||||
object : PagingSource<K, V>() {
|
||||
override suspend fun load(params: LoadParams<K>): LoadResult<K, V> {
|
||||
return loadData.invoke(params)
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<K, V>): K? {
|
||||
return initialKey
|
||||
}
|
||||
|
||||
}
|
||||
}.flow.cachedIn(viewModelScope)
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
package com.zj.wanandroid.data.bean
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
data class HomeThemeBean(
|
||||
var theme: AppTheme.Theme
|
||||
)
|
||||
|
||||
data class Hotkey(
|
||||
var link: String?,
|
||||
var name: String?,
|
||||
var order: Int,
|
||||
var visible: Int
|
||||
)
|
||||
|
||||
data class BasicBean<T>(
|
||||
var data: T?,
|
||||
var errorCode: Int,
|
||||
var errorMsg: String
|
||||
)
|
||||
|
||||
data class ListWrapper<T>(
|
||||
var curPage: Int,
|
||||
var offset: Int,
|
||||
var over: Boolean,
|
||||
var pageCount: Int,
|
||||
var size: Int,
|
||||
var total: Int,
|
||||
var datas: ArrayList<T>
|
||||
)
|
||||
|
||||
data class BannerBean(
|
||||
var desc: String?,
|
||||
var id: Int,
|
||||
var imagePath: String?,
|
||||
var isVisible: Int,
|
||||
var order: Int,
|
||||
var title: String?,
|
||||
var type: Int,
|
||||
var url: String?
|
||||
)
|
||||
|
||||
data class SharerBean<T>(
|
||||
val coinInfo: PointsBean,
|
||||
val shareArticles: ListWrapper<T>
|
||||
)
|
||||
|
||||
data class BasicUserInfo(
|
||||
val coinInfo: PointsBean,
|
||||
val userInfo: UserInfo
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class ParentBean(
|
||||
var children: MutableList<ParentBean>?,
|
||||
var courseId: Int = -1,
|
||||
var id: Int = -1,
|
||||
var name: String? = "分类",
|
||||
var order: Int = -1,
|
||||
var parentChapterId: Int = -1,
|
||||
var userControlSetTop: Boolean = false,
|
||||
var visible: Int = -1,
|
||||
var icon: String? = null,
|
||||
var link: String? = null
|
||||
) : Parcelable
|
||||
|
||||
data class NaviWrapper(
|
||||
var articles: MutableList<Article>?,
|
||||
var cid: Int,
|
||||
var name: String?
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class Article(
|
||||
var apkLink: String? = "",
|
||||
var audit: Int = -1,
|
||||
var author: String? = "作者",
|
||||
var canEdit: Boolean = false,
|
||||
var chapterId: Int = -1,
|
||||
var chapterName: String? = "章节",
|
||||
var collect: Boolean = false,
|
||||
var courseId: Int = -1,
|
||||
var desc: String? = "描述",
|
||||
var descMd: String? = "描述Md",
|
||||
var envelopePic: String? = "图片1",
|
||||
var fresh: Boolean = false,
|
||||
var host: String? = "https://www.wanandroid.com",
|
||||
var id: Int = -1,
|
||||
var link: String? = "https://www.wanandroid.com",
|
||||
var niceDate: String? = "1970-0-0",
|
||||
var niceShareDate: String? = "1970-0-0",
|
||||
var origin: String? = "",
|
||||
var prefix: String? = "",
|
||||
var projectLink: String? = "https://www.wanandroid.com",
|
||||
var publishTime: Long = 0L,
|
||||
var realSuperChapterId: Int = -1,
|
||||
var selfVisible: Int = -1,
|
||||
var shareDate: Long = 0L,
|
||||
var shareUser: String? = "分享者",
|
||||
var superChapterId: Int = -1,
|
||||
var superChapterName: String? = "超级分类",
|
||||
var tags: MutableList<ArticleTag>? = null,
|
||||
var title: String? = "标题",
|
||||
var type: Int = -1,
|
||||
var userId: Int = -1,
|
||||
var visible: Int = -1,
|
||||
var zan: Int = -1
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class ArticleTag(
|
||||
var name: String,
|
||||
var url: String
|
||||
) : Parcelable
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class WebData(
|
||||
var title: String?,
|
||||
var url: String
|
||||
) : Parcelable
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class UserInfo(
|
||||
var id: Int,
|
||||
var admin: Boolean,
|
||||
var chapterTops: MutableList<Int>,
|
||||
var coinCount: Int,
|
||||
var collectIds: MutableList<Int>,
|
||||
var email: String,
|
||||
var icon: String,
|
||||
var nickname: String,
|
||||
var password: String,
|
||||
var token: String,
|
||||
var type: Int,
|
||||
var username: String,
|
||||
) : Parcelable
|
||||
|
||||
data class PointsBean(
|
||||
var id: Int?,
|
||||
var coinCount: String,
|
||||
var level: Int?,
|
||||
var nickname: String,
|
||||
var rank: String?,
|
||||
var userId: Int,
|
||||
var username: String,
|
||||
var date: String?,
|
||||
var desc: String?,
|
||||
var reason: String?,
|
||||
var type: Int?,
|
||||
)
|
||||
|
||||
data class CollectBean(
|
||||
var author: String = "",
|
||||
var chapterId: Int = 0,
|
||||
var chapterName: String = "",
|
||||
var courseId: Int = 0,
|
||||
var desc: String = "",
|
||||
var envelopePic: String = "",
|
||||
var id: Int = 0,
|
||||
var link: String = "",
|
||||
var niceDate: String = "",
|
||||
var origin: String = "",
|
||||
var originId: Int = 0,
|
||||
var publishTime: Long = 0,
|
||||
var title: String = "",
|
||||
var userId: Int = 0,
|
||||
var visible: Int = 0,
|
||||
var zan: Int = 0
|
||||
)
|
||||
|
||||
|
||||
/********************* Begin: Welfare Data ***********************/
|
||||
abstract class GankBasedBean<T : Any?> {
|
||||
val page: Int = 1
|
||||
|
||||
@SerializedName("page_count")
|
||||
val pageSize: Int = 20
|
||||
val status: Int = 100
|
||||
|
||||
@SerializedName("total_counts")
|
||||
val totalSize: Int = 96
|
||||
abstract var data: T?
|
||||
}
|
||||
|
||||
data class WelfareBean(
|
||||
override var data: List<WelfareData>?
|
||||
) : GankBasedBean<List<WelfareData>>()
|
||||
|
||||
@Parcelize
|
||||
data class WelfareData(
|
||||
@SerializedName("_id") var id: String?,
|
||||
var author: String?,
|
||||
var category: String?,
|
||||
var createAt: String?,
|
||||
var desc: String?,
|
||||
var images: MutableList<String>,
|
||||
var likeCounts: Int,
|
||||
var publishedAt: String?,
|
||||
var stars: Int,
|
||||
var title: String?,
|
||||
var type: String?,
|
||||
var url: String?,
|
||||
var views: Int,
|
||||
) : Parcelable
|
||||
/********************* Begin: Welfare Data ***********************/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.zj.wanandroid.data.http
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.zj.wanandroid.data.http.interceptor.CacheCookieInterceptor
|
||||
import com.zj.wanandroid.data.http.interceptor.LogInterceptor
|
||||
import com.zj.wanandroid.data.http.interceptor.SetCookieInterceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.net.ssl.*
|
||||
|
||||
/**
|
||||
* Created by Superman. 19/5/27
|
||||
*/
|
||||
object ApiCall {
|
||||
|
||||
/**
|
||||
* 请求超时时间
|
||||
*/
|
||||
private const val DEFAULT_TIMEOUT = 30000
|
||||
private lateinit var SERVICE: HttpService
|
||||
|
||||
//手动创建一个OkHttpClient并设置超时时间
|
||||
val retrofit: HttpService
|
||||
get() {
|
||||
if (!ApiCall::SERVICE.isInitialized) {
|
||||
SERVICE = Retrofit.Builder()
|
||||
.client(okHttp)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.baseUrl(HttpService.url)
|
||||
.build()
|
||||
.create(HttpService::class.java)
|
||||
}
|
||||
return SERVICE
|
||||
}
|
||||
|
||||
//手动创建一个OkHttpClient并设置超时时间
|
||||
val okHttp: OkHttpClient
|
||||
get() {
|
||||
return OkHttpClient.Builder().run {
|
||||
connectTimeout(DEFAULT_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
|
||||
readTimeout(DEFAULT_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
|
||||
writeTimeout(DEFAULT_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
|
||||
addInterceptor(SetCookieInterceptor())
|
||||
addInterceptor(CacheCookieInterceptor())
|
||||
addInterceptor(LogInterceptor())
|
||||
//不验证证书
|
||||
sslSocketFactory(createSSLSocketFactory())
|
||||
hostnameVerifier(TrustAllNameVerifier())
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSSLSocketFactory(): SSLSocketFactory {
|
||||
lateinit var ssfFactory: SSLSocketFactory
|
||||
try {
|
||||
val sslFactory = SSLContext.getInstance("TLS")
|
||||
sslFactory.init(null, arrayOf(TrustAllCerts()), SecureRandom());
|
||||
ssfFactory = sslFactory.socketFactory
|
||||
} catch (e: Exception) {
|
||||
print("SSL错误:${e.message}")
|
||||
}
|
||||
return ssfFactory
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TrustAllNameVerifier: HostnameVerifier {
|
||||
@SuppressLint("BadHostnameVerifier")
|
||||
override fun verify(hostname: String?, session: SSLSession?): Boolean = true
|
||||
}
|
||||
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
class TrustAllCerts : X509TrustManager {
|
||||
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
|
||||
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.zj.wanandroid.data.http
|
||||
|
||||
import java.lang.Exception
|
||||
|
||||
sealed class HttpResult<out T> {
|
||||
|
||||
data class Success<T>(val result: T): HttpResult<T>()
|
||||
data class Error(val exception: Exception): HttpResult<Nothing>()
|
||||
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
package com.zj.wanandroid.data.http
|
||||
|
||||
import com.zj.wanandroid.data.bean.*
|
||||
import retrofit2.http.*
|
||||
|
||||
/**
|
||||
* 网络请求接口
|
||||
* 注意:接口前无需加斜杠
|
||||
* create by Mqz at 4/19
|
||||
*/
|
||||
interface HttpService {
|
||||
|
||||
companion object {
|
||||
const val url = "https://www.wanandroid.com"
|
||||
}
|
||||
|
||||
//首页
|
||||
@GET("/article/list/{page}/json")
|
||||
suspend fun getIndexList(@Path("page") page: Int): BasicBean<ListWrapper<Article>>
|
||||
|
||||
//广场
|
||||
@GET("/user_article/list/{page}/json")
|
||||
suspend fun getSquareData(@Path("page") page: Int): BasicBean<ListWrapper<Article>>
|
||||
|
||||
//问答
|
||||
@GET("/wenda/list/{page}/json")
|
||||
suspend fun getWendaData(@Path("page") page: Int): BasicBean<ListWrapper<Article>>
|
||||
|
||||
//置顶文章
|
||||
@GET("/article/top/json")
|
||||
suspend fun getTopArticles(): BasicBean<MutableList<Article>>
|
||||
|
||||
//搜索热词
|
||||
@GET("/hotkey/json")
|
||||
suspend fun getHotkeys(): BasicBean<MutableList<Hotkey>>
|
||||
|
||||
//banner
|
||||
@GET("/banner/json")
|
||||
suspend fun getBanners(): BasicBean<MutableList<BannerBean>>
|
||||
|
||||
//体系
|
||||
@GET("/tree/json")
|
||||
suspend fun getStructureList(): BasicBean<MutableList<ParentBean>>
|
||||
|
||||
//导航
|
||||
@GET("/navi/json")
|
||||
suspend fun getNavigationList(): BasicBean<MutableList<NaviWrapper>>
|
||||
|
||||
//公众号列表
|
||||
@GET("/wxarticle/chapters/json")
|
||||
suspend fun getPublicInformation(): BasicBean<MutableList<ParentBean>>
|
||||
|
||||
//某作者的公众号文章列表
|
||||
@GET("/wxarticle/list/{id}/{page}/json")
|
||||
suspend fun getPublicArticles(
|
||||
@Path("id") publicId: Int,
|
||||
@Path("page") page: Int
|
||||
): BasicBean<ListWrapper<Article>>
|
||||
|
||||
//某公众号作者的文章下搜索
|
||||
@GET("/wxarticle/list/{id}/{page}/json")
|
||||
suspend fun getPublicArticlesWithKey(
|
||||
@Path("id") publicId: Int,
|
||||
@Path("page") page: Int,
|
||||
@Query("k") key: String
|
||||
): BasicBean<ListWrapper<Article>>
|
||||
|
||||
//项目分类
|
||||
@GET("/project/tree/json")
|
||||
suspend fun getProjectCategory(): BasicBean<MutableList<ParentBean>>
|
||||
|
||||
//某个项目分类下的列表
|
||||
@GET("/project/list/{page}/json")
|
||||
suspend fun getProjects(
|
||||
@Path("page") page: Int,
|
||||
@Query("cid") cid: Int
|
||||
): BasicBean<ListWrapper<Article>>
|
||||
|
||||
//某个体系下的文章列表
|
||||
@GET("/article/list/{page}/json")
|
||||
suspend fun getStructureArticles(
|
||||
@Path("page") page: Int,
|
||||
@Query("cid") cid: Int
|
||||
): BasicBean<ListWrapper<Article>>
|
||||
|
||||
//在某个体系下搜索某作者的文章
|
||||
@GET("/article/list/{page}/json")
|
||||
suspend fun getArticlesByAuthor(
|
||||
@Path("page") page: Int,
|
||||
@Query("author") author: String
|
||||
): BasicBean<ListWrapper<Article>>
|
||||
|
||||
//热门项目
|
||||
@GET("/article/listproject/{page}/json")
|
||||
suspend fun getHotProjects(@Path("page") page: Int): BasicBean<ListWrapper<Article>>
|
||||
|
||||
//首页查询文章
|
||||
@FormUrlEncoded
|
||||
@POST("/article/query/{page}/json")
|
||||
suspend fun queryArticle(
|
||||
@Path("page") page: Int,
|
||||
@Field("k") key: String
|
||||
): BasicBean<ListWrapper<Article>>
|
||||
|
||||
//注册
|
||||
@FormUrlEncoded
|
||||
@POST("/user/register")
|
||||
suspend fun register(
|
||||
@Field("username") userName: String,
|
||||
@Field("password") password: String,
|
||||
@Field("repassword") repassword: String,
|
||||
): BasicBean<UserInfo>
|
||||
|
||||
//登录
|
||||
@FormUrlEncoded
|
||||
@POST("/user/login")
|
||||
suspend fun login(
|
||||
@Field("username") userName: String,
|
||||
@Field("password") password: String,
|
||||
): BasicBean<UserInfo>
|
||||
|
||||
//退出登录
|
||||
@GET("/user/logout/json")
|
||||
suspend fun logout(): BasicBean<Any>
|
||||
|
||||
//排行榜
|
||||
@GET("/coin/rank/{page}/json")
|
||||
suspend fun getPointsRankings(@Path("page") page: Int): BasicBean<ListWrapper<PointsBean>>
|
||||
|
||||
//积分记录
|
||||
@GET("/lg/coin/list/{page}/json")
|
||||
suspend fun getPointsRecords(@Path("page") page: Int): BasicBean<ListWrapper<PointsBean>>
|
||||
|
||||
//我的积分
|
||||
@GET("/lg/coin/userinfo/json")
|
||||
suspend fun getMyPointsRanking(): BasicBean<PointsBean>
|
||||
|
||||
//消息数量
|
||||
@GET("/message/lg/count_unread/json")
|
||||
suspend fun getMessageCount(): BasicBean<Int>
|
||||
|
||||
//未读消息列表
|
||||
@GET("/message/lg/unread_list/{page}/json")
|
||||
suspend fun getUnreadMessage(@Path("page") page: Int): BasicBean<ListWrapper<Any>>
|
||||
|
||||
//已读消息列表
|
||||
@GET("/message/lg/readed_list/{page}/json")
|
||||
suspend fun getReadedMessage(@Path("page") page: Int): BasicBean<ListWrapper<Any>>
|
||||
|
||||
//收藏列表
|
||||
@GET("/lg/collect/list/{page}/json")
|
||||
suspend fun getCollectionList(@Path("page") page: Int): BasicBean<ListWrapper<CollectBean>>
|
||||
|
||||
//收藏的网站
|
||||
@GET("/lg/collect/usertools/json")
|
||||
suspend fun getCollectUrls(): BasicBean<MutableList<ParentBean>>
|
||||
|
||||
//收藏站内文章
|
||||
@POST("/lg/collect/{id}/json")
|
||||
suspend fun collectInnerArticle(@Path("id") id: Int): BasicBean<Any>
|
||||
|
||||
//取消收藏站内文章(在首页等列表里取消)
|
||||
@POST("/lg/uncollect_originId/{id}/json")
|
||||
suspend fun uncollectInnerArticle(@Path("id") id: Int): BasicBean<Any>
|
||||
|
||||
//取消收藏站内文章(在收藏列表里取消)
|
||||
@POST("/lg/uncollect/{id}/json")
|
||||
suspend fun uncollectArticleById(
|
||||
@Path("id") id: Int,
|
||||
@Query("originId") originId: Int
|
||||
): BasicBean<Any>
|
||||
|
||||
//添加收藏网站
|
||||
@FormUrlEncoded
|
||||
@POST("/lg/collect/addtool/json")
|
||||
suspend fun addNewWebsiteCollect(
|
||||
@Field("name") title: String,
|
||||
@Field("link") linkUrl: String
|
||||
): BasicBean<ParentBean>
|
||||
|
||||
//添加站外文章
|
||||
@FormUrlEncoded
|
||||
@POST("/lg/collect/add/json")
|
||||
suspend fun addNewArticleCollect(
|
||||
@Field("title") title: String,
|
||||
@Field("link") linkUrl: String,
|
||||
@Field("author") author: String,
|
||||
): BasicBean<CollectBean>
|
||||
|
||||
//删除收藏网站
|
||||
@FormUrlEncoded
|
||||
@POST("/lg/collect/deletetool/json")
|
||||
suspend fun deleteWebsite(@Field("id") id: Int): BasicBean<Any>
|
||||
|
||||
//编辑收藏的网站
|
||||
@FormUrlEncoded
|
||||
@POST("/lg/collect/updatetool/json")
|
||||
suspend fun editCollectWebsite(
|
||||
@Field("id") id: Int,
|
||||
@Field("name") title: String,
|
||||
@Field("link") linkUrl: String,
|
||||
): BasicBean<Any>
|
||||
|
||||
//自己分享的文章
|
||||
@GET("/user/lg/private_articles/{page}/json")
|
||||
suspend fun getMyShareArticles(
|
||||
@Path("page") page: Int,
|
||||
@Query("page_size") size: Int = 40,
|
||||
): BasicBean<SharerBean<Article>>
|
||||
|
||||
//根据文章id删除自己分享的文章
|
||||
@POST("/lg/user_article/delete/{id}/json")
|
||||
suspend fun deleteMyShareArticle(@Path("id") id: Int): BasicBean<Any>
|
||||
|
||||
//分享文章
|
||||
@FormUrlEncoded
|
||||
@POST("/lg/user_article/add/json")
|
||||
suspend fun addMyShareArticle(
|
||||
@Field("title") title: String,
|
||||
@Field("link") linkUrl: String,
|
||||
@Field("shareUser") shareUser: String,
|
||||
): BasicBean<Any>
|
||||
|
||||
//其他作者分享的文章
|
||||
@GET("/user/{userId}/share_articles/{page}/json")
|
||||
suspend fun getAuthorShareArticles(
|
||||
@Path("userId") userId: Int,
|
||||
@Path("page") page: Int,
|
||||
@Query("page_size") size: Int = 40,
|
||||
): BasicBean<SharerBean<Article>>
|
||||
|
||||
@GET("/user/lg/userinfo/json")
|
||||
suspend fun getBasicUserInfo(): BasicBean<BasicUserInfo>
|
||||
|
||||
// 福利
|
||||
@GET("https://gank.io/api/v2/data/category/{category}/type/{type}/page/{page}/count/{pageSize}")
|
||||
suspend fun getWelfareList(
|
||||
@Path(value = "category", encoded = false) category: String = "Girl",
|
||||
@Path(value = "type", encoded = false) type: String = "Girl",
|
||||
@Path("page") page: Int,
|
||||
@Path("pageSize") pageCount: Int = 40
|
||||
): WelfareBean
|
||||
|
||||
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.zj.wanandroid.data.http
|
||||
|
||||
sealed class PageState {
|
||||
object Loading : PageState()
|
||||
data class Success(val isEmpty: Boolean) : PageState()
|
||||
data class Error(val exception: Throwable) : PageState()
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.zj.wanandroid.data.http.interceptor
|
||||
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class CacheCookieInterceptor: Interceptor {
|
||||
|
||||
private val loginUrl = "user/login"
|
||||
private val registerUrl = "user/register"
|
||||
private val SET_COOKIE_KEY = "set-cookie"
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
val requestUrl = request.url().toString()
|
||||
val domain = request.url().host()
|
||||
if (aboutUser(requestUrl)) {
|
||||
val cookies = response.headers(SET_COOKIE_KEY)
|
||||
if (cookies.isNotEmpty()) {
|
||||
//cookie可能有多个,都保存下来
|
||||
DataStoreUtils.putSyncData(domain, encodeCookie(cookies))
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
private fun aboutUser(url: String): Boolean = url.contains(loginUrl) or url.contains(registerUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* 整理cookie
|
||||
*/
|
||||
private fun encodeCookie(cookies: List<String>): String {
|
||||
val sb = StringBuilder()
|
||||
val set = HashSet<String>()
|
||||
cookies
|
||||
.map { cookie ->
|
||||
cookie.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
}
|
||||
.forEach { it ->
|
||||
it.filterNot { set.contains(it) }.forEach { set.add(it) }
|
||||
}
|
||||
|
||||
val ite = set.iterator()
|
||||
while (ite.hasNext()) {
|
||||
val cookie = ite.next()
|
||||
sb.append(cookie).append(";")
|
||||
}
|
||||
|
||||
val last = sb.lastIndexOf(";")
|
||||
if (sb.length - 1 == last) {
|
||||
sb.deleteCharAt(last)
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package com.zj.wanandroid.data.http.interceptor
|
||||
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import okhttp3.*
|
||||
import java.io.IOException
|
||||
import java.net.URLDecoder
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by Superman on 2021/1/29.
|
||||
*/
|
||||
class LogInterceptor @Inject constructor() : Interceptor {
|
||||
|
||||
private val logTag = "http ## "
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response? {
|
||||
val request = chain.request()
|
||||
return kotlin.runCatching { chain.proceed(request) }
|
||||
.onSuccess {
|
||||
logRequest(request, chain.connection())
|
||||
if (it.isSuccessful) {
|
||||
logResponse(it)
|
||||
} else {
|
||||
logThat(ColorLevel.WARN(it.message() ?: "未知异常"))
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
logThat(ColorLevel.ERROR(it.message ?: "未知异常"))
|
||||
}
|
||||
.getOrThrow()
|
||||
}
|
||||
|
||||
private fun logResponse(response: Response) {
|
||||
val strb = StringBuffer()
|
||||
strb.appendLine("\r\n")
|
||||
strb.appendLine("<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-")
|
||||
|
||||
var headerText = ""
|
||||
response.headers().toMultimap().forEach { header->
|
||||
headerText += "请求 Header:{${header.key}=${header.value}}\n"
|
||||
}
|
||||
strb.appendln(headerText)
|
||||
kotlin.runCatching {
|
||||
//peek类似于clone数据流,监视,窥探,不能直接用原来的body的string流数据作为日志,会消费掉io,所有这里是peek,监测
|
||||
val peekBody: ResponseBody = response.peekBody(1024 * 1024)
|
||||
strb.appendln(peekBody.string())
|
||||
}.getOrNull()
|
||||
|
||||
strb.appendLine(
|
||||
"<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<" +
|
||||
"-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-"
|
||||
)
|
||||
logThat(ColorLevel.INFO(strb.toString()))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 打印日志
|
||||
*/
|
||||
private fun logThat(tempLevel: ColorLevel) {
|
||||
when (tempLevel) {
|
||||
is ColorLevel.VERBOSE -> Log.v(logTag, tempLevel.logText)
|
||||
is ColorLevel.DEBUG -> Log.d(logTag, tempLevel.logText)
|
||||
is ColorLevel.INFO -> Log.i(logTag, tempLevel.logText)
|
||||
is ColorLevel.WARN -> Log.w(logTag, tempLevel.logText)
|
||||
is ColorLevel.ERROR -> Log.e(logTag, tempLevel.logText)
|
||||
}
|
||||
}
|
||||
|
||||
private fun logRequest(request: Request, connection: Connection?) {
|
||||
val strb = StringBuilder()
|
||||
strb.appendLine("\r\n")
|
||||
strb.appendLine(
|
||||
"->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->"
|
||||
)
|
||||
logHeaders(strb, request, connection)
|
||||
strb.appendLine("RequestBody:${request.body().toString()}")
|
||||
logThat(ColorLevel.VERBOSE(strb.toString()))
|
||||
}
|
||||
|
||||
private fun logHeaders(strb: StringBuilder, request: Request, connection: Connection?) {
|
||||
logBasic(strb, request, connection)
|
||||
var headerStr = ""
|
||||
request.headers().toMultimap().forEach { header->
|
||||
headerStr += "请求 Header:{${header.key}=${header.value}}\n"
|
||||
}
|
||||
strb.appendln(headerStr)
|
||||
}
|
||||
|
||||
private fun logBasic(strb: StringBuilder, request: Request, connection: Connection?) {
|
||||
strb.appendLine(
|
||||
"请求 method:${request.method()} url:${decodeUrlStr(request.url().toString())} tag:" +
|
||||
"${request.tag()} protocol:${connection?.protocol() ?: Protocol.HTTP_1_1}"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 对于url编码的string解码
|
||||
*/
|
||||
private fun decodeUrlStr(url: String): String? {
|
||||
return kotlin.runCatching {
|
||||
URLDecoder.decode(url, "utf-8")
|
||||
}.onFailure { it.printStackTrace() }.getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印日志范围
|
||||
*/
|
||||
enum class LogLevel {
|
||||
NONE,//不打印
|
||||
BASIC,//只打印行首,请求/响应
|
||||
HEADERS, //打印请求和响应的所有header
|
||||
BODY //打印所有
|
||||
}
|
||||
|
||||
/**
|
||||
* Log颜色等级,应用于android Logcat分为 v、d、i、w、e
|
||||
*/
|
||||
sealed class ColorLevel {
|
||||
data class VERBOSE(val logText: String): ColorLevel()
|
||||
data class DEBUG(val logText: String): ColorLevel()
|
||||
data class INFO(val logText: String): ColorLevel()
|
||||
data class WARN(val logText: String): ColorLevel()
|
||||
data class ERROR(val logText: String): ColorLevel()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.zj.wanandroid.data.http.interceptor
|
||||
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class SetCookieInterceptor: Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val builder = request.newBuilder()
|
||||
val domain = request.url().host()
|
||||
//获取domain内的cookie
|
||||
if (domain.isNotEmpty()) {
|
||||
val cookie: String = DataStoreUtils.readStringData(domain, "")
|
||||
if (cookie.isNotEmpty()) {
|
||||
builder.addHeader("Cookie", cookie)
|
||||
}
|
||||
}
|
||||
return chain.proceed(builder.build())
|
||||
}
|
||||
}
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
package com.zj.wanandroid.data.store
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.*
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.clear
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.clearSync
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.getData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.getSyncData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.putData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.putSyncData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.readBooleanData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.readBooleanFlow
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.readFloatData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.readFloatFlow
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.readIntData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.readIntFlow
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.readLongData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.readLongFlow
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.readStringData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.readStringFlow
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.saveBooleanData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.saveFloatData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.saveIntData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.saveLongData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.saveStringData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.saveSyncBooleanData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.saveSyncFloatData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.saveSyncIntData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.saveSyncLongData
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils.saveSyncStringData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* 版权:Zhujiang 个人版权
|
||||
*
|
||||
* @author zhujiang
|
||||
* 创建日期:12/3/20
|
||||
*
|
||||
* 异步获取数据
|
||||
* [getData] [readBooleanFlow] [readFloatFlow] [readIntFlow] [readLongFlow] [readStringFlow]
|
||||
* 同步获取数据
|
||||
* [getSyncData] [readBooleanData] [readFloatData] [readIntData] [readLongData] [readStringData]
|
||||
*
|
||||
* 异步写入数据
|
||||
* [putData] [saveBooleanData] [saveFloatData] [saveIntData] [saveLongData] [saveStringData]
|
||||
* 同步写入数据
|
||||
* [putSyncData] [saveSyncBooleanData] [saveSyncFloatData] [saveSyncIntData] [saveSyncLongData] [saveSyncStringData]
|
||||
*
|
||||
* 异步清除数据
|
||||
* [clear]
|
||||
* 同步清除数据
|
||||
* [clearSync]
|
||||
*
|
||||
* 描述:DataStore 工具类
|
||||
*
|
||||
*/
|
||||
object DataStoreUtils {
|
||||
|
||||
private const val preferenceName = "WanAndroidDataStore"
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(preferenceName)
|
||||
|
||||
private lateinit var dataStore: DataStore<Preferences>
|
||||
|
||||
/**
|
||||
* init Context
|
||||
* @param context Context
|
||||
*/
|
||||
fun init(context: Context) {
|
||||
dataStore = context.dataStore
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <U> getSyncData(key: String, default: U): U {
|
||||
val res = when (default) {
|
||||
is Long -> readLongData(key, default)
|
||||
is String -> readStringData(key, default)
|
||||
is Int -> readIntData(key, default)
|
||||
is Boolean -> readBooleanData(key, default)
|
||||
is Float -> readFloatData(key, default)
|
||||
else -> throw IllegalArgumentException("This type can be saved into DataStore")
|
||||
}
|
||||
return res as U
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <U> getData(key: String, default: U): Flow<U> {
|
||||
val data = when (default) {
|
||||
is Long -> readLongFlow(key, default)
|
||||
is String -> readStringFlow(key, default)
|
||||
is Int -> readIntFlow(key, default)
|
||||
is Boolean -> readBooleanFlow(key, default)
|
||||
is Float -> readFloatFlow(key, default)
|
||||
else -> throw IllegalArgumentException("This type can be saved into DataStore")
|
||||
}
|
||||
return data as Flow<U>
|
||||
}
|
||||
|
||||
suspend fun <U> putData(key: String, value: U) {
|
||||
when (value) {
|
||||
is Long -> saveLongData(key, value)
|
||||
is String -> saveStringData(key, value)
|
||||
is Int -> saveIntData(key, value)
|
||||
is Boolean -> saveBooleanData(key, value)
|
||||
is Float -> saveFloatData(key, value)
|
||||
else -> throw IllegalArgumentException("This type can be saved into DataStore")
|
||||
}
|
||||
}
|
||||
|
||||
fun <U> putSyncData(key: String, value: U) {
|
||||
when (value) {
|
||||
is Long -> saveSyncLongData(key, value)
|
||||
is String -> saveSyncStringData(key, value)
|
||||
is Int -> saveSyncIntData(key, value)
|
||||
is Boolean -> saveSyncBooleanData(key, value)
|
||||
is Float -> saveSyncFloatData(key, value)
|
||||
else -> throw IllegalArgumentException("This type can be saved into DataStore")
|
||||
}
|
||||
}
|
||||
|
||||
fun readBooleanFlow(key: String, default: Boolean = false): Flow<Boolean> =
|
||||
dataStore.data
|
||||
.catch {
|
||||
//当读取数据遇到错误时,如果是 `IOException` 异常,发送一个 emptyPreferences 来重新使用
|
||||
//但是如果是其他的异常,最好将它抛出去,不要隐藏问题
|
||||
if (it is IOException) {
|
||||
it.printStackTrace()
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}.map {
|
||||
it[booleanPreferencesKey(key)] ?: default
|
||||
}
|
||||
|
||||
fun readBooleanData(key: String, default: Boolean = false): Boolean {
|
||||
var value = false
|
||||
runBlocking {
|
||||
dataStore.data.first {
|
||||
value = it[booleanPreferencesKey(key)] ?: default
|
||||
true
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fun readIntFlow(key: String, default: Int = 0): Flow<Int> =
|
||||
dataStore.data
|
||||
.catch {
|
||||
if (it is IOException) {
|
||||
it.printStackTrace()
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}.map {
|
||||
it[intPreferencesKey(key)] ?: default
|
||||
}
|
||||
|
||||
fun readIntData(key: String, default: Int = 0): Int {
|
||||
var value = 0
|
||||
runBlocking {
|
||||
dataStore.data.first {
|
||||
value = it[intPreferencesKey(key)] ?: default
|
||||
true
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fun readStringFlow(key: String, default: String = ""): Flow<String> =
|
||||
dataStore.data
|
||||
.catch {
|
||||
if (it is IOException) {
|
||||
it.printStackTrace()
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}.map {
|
||||
it[stringPreferencesKey(key)] ?: default
|
||||
}
|
||||
|
||||
fun readStringData(key: String, default: String = ""): String {
|
||||
var value = ""
|
||||
runBlocking {
|
||||
dataStore.data.first {
|
||||
value = it[stringPreferencesKey(key)] ?: default
|
||||
true
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fun readFloatFlow(key: String, default: Float = 0f): Flow<Float> =
|
||||
dataStore.data
|
||||
.catch {
|
||||
if (it is IOException) {
|
||||
it.printStackTrace()
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}.map {
|
||||
it[floatPreferencesKey(key)] ?: default
|
||||
}
|
||||
|
||||
fun readFloatData(key: String, default: Float = 0f): Float {
|
||||
var value = 0f
|
||||
runBlocking {
|
||||
dataStore.data.first {
|
||||
value = it[floatPreferencesKey(key)] ?: default
|
||||
true
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fun readLongFlow(key: String, default: Long = 0L): Flow<Long> =
|
||||
dataStore.data
|
||||
.catch {
|
||||
if (it is IOException) {
|
||||
it.printStackTrace()
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}.map {
|
||||
it[longPreferencesKey(key)] ?: default
|
||||
}
|
||||
|
||||
fun readLongData(key: String, default: Long = 0L): Long {
|
||||
var value = 0L
|
||||
runBlocking {
|
||||
dataStore.data.first {
|
||||
value = it[longPreferencesKey(key)] ?: default
|
||||
true
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
suspend fun saveBooleanData(key: String, value: Boolean) {
|
||||
dataStore.edit { mutablePreferences ->
|
||||
mutablePreferences[booleanPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSyncBooleanData(key: String, value: Boolean) =
|
||||
runBlocking { saveBooleanData(key, value) }
|
||||
|
||||
suspend fun saveIntData(key: String, value: Int) {
|
||||
dataStore.edit { mutablePreferences ->
|
||||
mutablePreferences[intPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSyncIntData(key: String, value: Int) = runBlocking { saveIntData(key, value) }
|
||||
|
||||
suspend fun saveStringData(key: String, value: String) {
|
||||
dataStore.edit { mutablePreferences ->
|
||||
mutablePreferences[stringPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSyncStringData(key: String, value: String) = runBlocking { saveStringData(key, value) }
|
||||
|
||||
suspend fun saveFloatData(key: String, value: Float) {
|
||||
dataStore.edit { mutablePreferences ->
|
||||
mutablePreferences[floatPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSyncFloatData(key: String, value: Float) = runBlocking { saveFloatData(key, value) }
|
||||
|
||||
suspend fun saveLongData(key: String, value: Long) {
|
||||
dataStore.edit { mutablePreferences ->
|
||||
mutablePreferences[longPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSyncLongData(key: String, value: Long) = runBlocking { saveLongData(key, value) }
|
||||
|
||||
suspend fun clear() {
|
||||
dataStore.edit {
|
||||
it.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSync() {
|
||||
runBlocking {
|
||||
dataStore.edit {
|
||||
it.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.zj.wanandroid.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.zj.wanandroid.MyApp
|
||||
import com.zj.wanandroid.R
|
||||
|
||||
val Transparent = Color(0x00000000)
|
||||
|
||||
val themeColor = Color(MyApp.CONTEXT.resources.getColor(R.color.primary))
|
||||
val splashText = Color(0x25000000)
|
||||
val white = Color(0xFFFFFFFF)
|
||||
val white1 = Color(0xFFF7F7F7)
|
||||
val white2 = Color(0xFFEDEDED)
|
||||
val white3 = Color(0xFFE5E5E5)
|
||||
val white4 = Color(0xFFD5D5D5)
|
||||
val white5 = Color(0xFFCCCCCC)
|
||||
val black = Color(0xFF000000)
|
||||
val black1 = Color(0xFF1E1E1E)
|
||||
val black2 = Color(0xFF111111)
|
||||
val black3 = Color(0xFF191919)
|
||||
val black4 = Color(0xFF252525)
|
||||
val black5 = Color(0xFF2C2C2C)
|
||||
val black6 = Color(0xFF07130A)
|
||||
val black7 = Color(0xFF292929)
|
||||
val grey1 = Color(0xFF888888)
|
||||
val grey2 = Color(0xFFCCC7BF)
|
||||
val grey3 = Color(0xFF767676)
|
||||
val grey4 = Color(0xFFB2B2B2)
|
||||
val grey5 = Color(0xFF5E5E5E)
|
||||
val green1 = Color(0xFFB0EB6E)
|
||||
val green2 = Color(0xFF6DB476)
|
||||
val green3 = Color(0xFF67BF63)
|
||||
val red = Color(0xFFFF0000)
|
||||
val red1 = Color(0xFFDF5554)
|
||||
val red2 = Color(0xFFDD302E)
|
||||
val red3 = Color(0xFFF77B7A)
|
||||
val red4 = Color(0xFFD42220)
|
||||
val red5 = Color(0xFFC51614)
|
||||
val red6 = Color(0xFFF74D4B)
|
||||
val red7 = Color(0xFFDC514E)
|
||||
val red8 = Color(0xFFCBC7BF)
|
||||
val yellow1 = Color(0xFFF6CA23)
|
||||
val blue = Color(0xFF0000FF)
|
||||
val info = Color(0xFF018786)
|
||||
val warn = Color(0xFFD87831)
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.zj.wanandroid.theme
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
val ToolBarHeight = 48.dp
|
||||
val TabBarHeight = 48.dp
|
||||
val SearchBarHeight = 42.dp
|
||||
val BottomNavBarHeight = 56.dp
|
||||
val ListTitleHeight = 30.dp
|
||||
|
||||
val PrimaryButtonHeight = 36.dp
|
||||
val MediumButtonHeight = 28.dp
|
||||
val SmallButtonHeight = 28.dp
|
||||
|
||||
|
||||
val H1 = 48.sp //超大号标题
|
||||
val H2 = 36.sp //大号标题
|
||||
val H3 = 24.sp //主标题
|
||||
val H4 = 20.sp //普通标题
|
||||
val H5 = 16.sp //内容文本
|
||||
val H6 = 14.sp //普通文字尺寸
|
||||
val H7 = 12.sp //提示语尺寸
|
||||
|
||||
val ToolBarTitleSize = 18.sp
|
||||
|
||||
val cardCorner = 5.dp //卡片的圆角
|
||||
val buttonCorner = 3.dp //按钮的圆角
|
||||
val buttonHeight = 36.dp //按钮的高度
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.zj.wanandroid.theme
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Shapes
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val AppShapes = Shapes(
|
||||
small = RoundedCornerShape(4.dp),
|
||||
medium = RoundedCornerShape(4.dp),
|
||||
large = RoundedCornerShape(0.dp)
|
||||
)
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.zj.wanandroid.theme
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
|
||||
//夜色主题
|
||||
private val DarkColorPalette = AppColors(
|
||||
themeUi = black1,
|
||||
background = black2,
|
||||
listItem = black3,
|
||||
divider = black4,
|
||||
textPrimary = white4,
|
||||
textSecondary = grey1,
|
||||
mainColor = white,
|
||||
card = white1,
|
||||
icon = white4,
|
||||
info = info,
|
||||
warn = warn,
|
||||
success = green3,
|
||||
error = red2,
|
||||
primaryBtnBg = black1,
|
||||
secondBtnBg = white1,
|
||||
hot = red,
|
||||
placeholder = grey1,
|
||||
)
|
||||
|
||||
//白天主题
|
||||
private val LightColorPalette = AppColors(
|
||||
themeUi = themeColor,
|
||||
background = white2,
|
||||
listItem = white,
|
||||
divider = white3,
|
||||
textPrimary = black3,
|
||||
textSecondary = grey1,
|
||||
mainColor = white,
|
||||
card = white1,
|
||||
icon = white4,
|
||||
info = info,
|
||||
warn = warn,
|
||||
success = green3,
|
||||
error = red2,
|
||||
primaryBtnBg = themeColor,
|
||||
secondBtnBg = white3,
|
||||
hot = red,
|
||||
placeholder = white3,
|
||||
)
|
||||
var LocalAppColors = compositionLocalOf {
|
||||
LightColorPalette
|
||||
}
|
||||
|
||||
@Stable
|
||||
object AppTheme {
|
||||
val colors: AppColors
|
||||
@Composable
|
||||
get() = LocalAppColors.current
|
||||
|
||||
enum class Theme {
|
||||
Light, Dark
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class AppColors(
|
||||
themeUi: Color,
|
||||
background: Color,
|
||||
listItem: Color,
|
||||
divider: Color,
|
||||
textPrimary: Color,
|
||||
textSecondary: Color,
|
||||
mainColor: Color,
|
||||
card: Color,
|
||||
icon: Color,
|
||||
info: Color,
|
||||
warn: Color,
|
||||
success: Color,
|
||||
error: Color,
|
||||
primaryBtnBg: Color,
|
||||
secondBtnBg: Color,
|
||||
hot: Color,
|
||||
placeholder: Color,
|
||||
) {
|
||||
var themeUi: Color by mutableStateOf(themeUi)
|
||||
internal set
|
||||
var background: Color by mutableStateOf(background)
|
||||
private set
|
||||
var listItem: Color by mutableStateOf(listItem)
|
||||
private set
|
||||
var divider: Color by mutableStateOf(divider)
|
||||
private set
|
||||
var textPrimary: Color by mutableStateOf(textPrimary)
|
||||
internal set
|
||||
var textSecondary: Color by mutableStateOf(textSecondary)
|
||||
private set
|
||||
var mainColor: Color by mutableStateOf(mainColor)
|
||||
internal set
|
||||
var card: Color by mutableStateOf(card)
|
||||
private set
|
||||
var icon: Color by mutableStateOf(icon)
|
||||
private set
|
||||
var info: Color by mutableStateOf(info)
|
||||
private set
|
||||
var warn: Color by mutableStateOf(warn)
|
||||
private set
|
||||
var success: Color by mutableStateOf(success)
|
||||
private set
|
||||
var error: Color by mutableStateOf(error)
|
||||
private set
|
||||
var primaryBtnBg: Color by mutableStateOf(primaryBtnBg)
|
||||
internal set
|
||||
var secondBtnBg: Color by mutableStateOf(secondBtnBg)
|
||||
private set
|
||||
var hot: Color by mutableStateOf(hot)
|
||||
private set
|
||||
var placeholder: Color by mutableStateOf(placeholder)
|
||||
private set
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
theme: AppTheme.Theme = AppTheme.Theme.Light,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
|
||||
val targetColors = when (theme) {
|
||||
AppTheme.Theme.Light -> {
|
||||
LightColorPalette.themeUi = themeColor
|
||||
LightColorPalette.primaryBtnBg = themeColor
|
||||
LightColorPalette
|
||||
}
|
||||
AppTheme.Theme.Dark -> DarkColorPalette
|
||||
}
|
||||
|
||||
val themeUi = animateColorAsState(targetColors.themeUi, TweenSpec(600))
|
||||
val background = animateColorAsState(targetColors.background, TweenSpec(600))
|
||||
val listItem = animateColorAsState(targetColors.listItem, TweenSpec(600))
|
||||
val divider = animateColorAsState(targetColors.divider, TweenSpec(600))
|
||||
val textPrimary = animateColorAsState(targetColors.textPrimary, TweenSpec(600))
|
||||
val textSecondary = animateColorAsState(targetColors.textSecondary, TweenSpec(600))
|
||||
val mainColor = animateColorAsState(targetColors.mainColor, TweenSpec(600))
|
||||
val card = animateColorAsState(targetColors.card, TweenSpec(600))
|
||||
val icon = animateColorAsState(targetColors.icon, TweenSpec(600))
|
||||
val info = animateColorAsState(targetColors.info, TweenSpec(600))
|
||||
val warn = animateColorAsState(targetColors.warn, TweenSpec(600))
|
||||
val success = animateColorAsState(targetColors.success, TweenSpec(600))
|
||||
val error = animateColorAsState(targetColors.error, TweenSpec(600))
|
||||
val primaryBtnBg = animateColorAsState(targetColors.primaryBtnBg, TweenSpec(600))
|
||||
val secondBtnBg = animateColorAsState(targetColors.secondBtnBg, TweenSpec(600))
|
||||
val hot = animateColorAsState(targetColors.hot, TweenSpec(600))
|
||||
val placeholder = animateColorAsState(targetColors.placeholder, TweenSpec(600))
|
||||
val appColors = AppColors(
|
||||
themeUi = themeUi.value,
|
||||
background = background.value,
|
||||
listItem = listItem.value,
|
||||
divider = divider.value,
|
||||
textPrimary = textPrimary.value,
|
||||
textSecondary = textSecondary.value,
|
||||
mainColor = mainColor.value,
|
||||
card = card.value,
|
||||
icon = icon.value,
|
||||
primaryBtnBg = primaryBtnBg.value,
|
||||
secondBtnBg = secondBtnBg.value,
|
||||
info = info.value,
|
||||
warn = warn.value,
|
||||
success = success.value,
|
||||
error = error.value,
|
||||
hot = hot.value,
|
||||
placeholder = placeholder.value
|
||||
)
|
||||
|
||||
val systemUiCtrl = rememberSystemUiController()
|
||||
systemUiCtrl.setStatusBarColor(appColors.themeUi)
|
||||
systemUiCtrl.setNavigationBarColor(appColors.themeUi)
|
||||
systemUiCtrl.setSystemBarsColor(appColors.themeUi)
|
||||
|
||||
ProvideWindowInsets {
|
||||
CompositionLocalProvider(LocalAppColors provides appColors, content = content)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.zj.wanandroid.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
|
||||
)
|
||||
*/
|
||||
)
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package com.zj.wanandroid.ui.page.common
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.SnackbarHost
|
||||
import androidx.compose.material.rememberScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.google.accompanist.insets.navigationBarsPadding
|
||||
import com.google.accompanist.insets.statusBarsPadding
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.zj.wanandroid.data.bean.WebData
|
||||
import com.zj.wanandroid.ui.page.login.LoginPage
|
||||
import com.zj.wanandroid.ui.page.main.category.CategoryPage
|
||||
import com.zj.wanandroid.ui.page.main.collect.CollectPage
|
||||
import com.zj.wanandroid.ui.page.main.home.HomePage
|
||||
import com.zj.wanandroid.ui.page.main.profile.ProfilePage
|
||||
import com.zj.wanandroid.ui.page.search.SearchPage
|
||||
import com.zj.wanandroid.ui.page.webview.WebViewPage
|
||||
import com.zj.wanandroid.ui.widgets.AppSnackBar
|
||||
import com.zj.wanandroid.ui.widgets.BottomNavBarView
|
||||
import com.zj.wanandroid.utils.RouteUtils
|
||||
import com.zj.wanandroid.utils.fromJson
|
||||
|
||||
@ExperimentalComposeUiApi
|
||||
@ExperimentalFoundationApi
|
||||
@ExperimentalPagerApi
|
||||
@Composable
|
||||
fun AppScaffold() {
|
||||
val navCtrl = rememberNavController()
|
||||
val navBackStackEntry by navCtrl.currentBackStackEntryAsState()
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding(),
|
||||
bottomBar = {
|
||||
when (currentDestination?.route) {
|
||||
RouteName.HOME -> BottomNavBarView(navCtrl = navCtrl)
|
||||
RouteName.CATEGORY -> BottomNavBarView(navCtrl = navCtrl)
|
||||
RouteName.COLLECTION -> BottomNavBarView(navCtrl = navCtrl)
|
||||
RouteName.PROFILE -> BottomNavBarView(navCtrl = navCtrl)
|
||||
}
|
||||
},
|
||||
content = {
|
||||
var homeIndex = remember { 0 }
|
||||
var categoryIndex = remember { 0 }
|
||||
|
||||
NavHost(
|
||||
modifier = Modifier.background(MaterialTheme.colors.background),
|
||||
navController = navCtrl,
|
||||
startDestination = RouteName.HOME
|
||||
) {
|
||||
//首页
|
||||
composable(route = RouteName.HOME) {
|
||||
HomePage(navCtrl, scaffoldState)
|
||||
}
|
||||
|
||||
//分类
|
||||
composable(route = RouteName.CATEGORY) {
|
||||
CategoryPage(navCtrl)
|
||||
}
|
||||
|
||||
//收藏
|
||||
composable(route = RouteName.COLLECTION) {
|
||||
CollectPage(navCtrl, scaffoldState)
|
||||
}
|
||||
|
||||
//我的
|
||||
composable(route = RouteName.PROFILE) {
|
||||
ProfilePage(navCtrl, scaffoldState)
|
||||
}
|
||||
|
||||
//WebView
|
||||
composable(route = RouteName.WEB_VIEW + "/{webData}",
|
||||
arguments = listOf(navArgument("webData") { type = NavType.StringType })
|
||||
) {
|
||||
val args = it.arguments?.getString("webData")?.fromJson<WebData>()
|
||||
if (args != null) {
|
||||
WebViewPage(webData = args, navCtrl = navCtrl)
|
||||
}
|
||||
}
|
||||
|
||||
//登录
|
||||
composable(route = RouteName.LOGIN) {
|
||||
LoginPage(navCtrl, scaffoldState)
|
||||
}
|
||||
|
||||
//文章搜索页
|
||||
composable(
|
||||
route = RouteName.ARTICLE_SEARCH + "/{id}",
|
||||
arguments = listOf(navArgument("id") { type = NavType.IntType })
|
||||
) {
|
||||
SearchPage(navCtrl, scaffoldState)
|
||||
}
|
||||
}
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(
|
||||
hostState = scaffoldState.snackbarHostState
|
||||
) { data ->
|
||||
println("actionLabel = ${data.actionLabel}")
|
||||
AppSnackBar(data = data)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.zj.wanandroid.ui.page.common
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.zj.wanandroid.R
|
||||
|
||||
sealed class BottomNavRoute(
|
||||
var routeName: String,
|
||||
@StringRes var stringId: Int,
|
||||
var icon: ImageVector
|
||||
) {
|
||||
object Home: BottomNavRoute(RouteName.HOME, R.string.home, Icons.Default.Home)
|
||||
object Category: BottomNavRoute(RouteName.CATEGORY, R.string.category, Icons.Default.Menu)
|
||||
object Collection: BottomNavRoute(RouteName.COLLECTION, R.string.collection, Icons.Default.Favorite)
|
||||
object Profile: BottomNavRoute(RouteName.PROFILE, R.string.profile, Icons.Default.Person)
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.zj.wanandroid.ui.page.common
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import com.zj.wanandroid.ui.page.splash.SplashPage
|
||||
|
||||
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
@ExperimentalComposeUiApi
|
||||
@Composable
|
||||
fun HomeEntry() {
|
||||
//是否闪屏页
|
||||
var isSplash by remember { mutableStateOf(true) }
|
||||
AppTheme {
|
||||
if (isSplash) {
|
||||
SplashPage { isSplash = false }
|
||||
} else {
|
||||
AppScaffold()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.zj.wanandroid.ui.page.common
|
||||
|
||||
object RouteName {
|
||||
const val HOME = "home"
|
||||
const val CATEGORY = "category"
|
||||
const val COLLECTION = "collection"
|
||||
const val PROFILE = "profile"
|
||||
const val WEB_VIEW = "web_view"
|
||||
const val LOGIN = "login"
|
||||
const val ARTICLE_SEARCH = "article_search"
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package com.zj.wanandroid.ui.page.login
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import com.zj.wanandroid.theme.ToolBarHeight
|
||||
import com.zj.wanandroid.ui.widgets.*
|
||||
import com.zj.wanandroid.utils.RouteUtils.back
|
||||
import kotlinx.coroutines.flow.collect
|
||||
|
||||
@ExperimentalComposeUiApi
|
||||
@Composable
|
||||
fun LoginPage(
|
||||
navCtrl: NavHostController,
|
||||
scaffoldState: ScaffoldState,
|
||||
viewModel: LoginViewModel = hiltViewModel()
|
||||
) {
|
||||
val viewStates = viewModel.viewStates
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val coroutineState = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.viewEvents.collect {
|
||||
if (it is LoginViewEvent.PopBack) {
|
||||
navCtrl.popBackStack()
|
||||
} else if (it is LoginViewEvent.ErrorMessage) {
|
||||
popupSnackBar(coroutineState, scaffoldState, label = SNACK_ERROR, it.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppTheme.colors.themeUi)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
)
|
||||
},
|
||||
) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 80.dp)
|
||||
.fillMaxWidth()
|
||||
.height(ToolBarHeight)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = AppTheme.colors.mainColor,
|
||||
modifier = Modifier
|
||||
.padding(start = 20.dp)
|
||||
.align(Alignment.CenterStart)
|
||||
.clickable { navCtrl.back() }
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
LargeTitle(
|
||||
title = "WanAndroid",
|
||||
modifier = Modifier
|
||||
.padding(bottom = 50.dp)
|
||||
.align(Alignment.Center),
|
||||
color = AppTheme.colors.mainColor
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
LoginEditView(
|
||||
text = viewStates.account,
|
||||
labelText = "账号",
|
||||
hintText = "请输入用户名",
|
||||
onValueChanged = { viewModel.dispatch(LoginViewAction.UpdateAccount(it)) },
|
||||
onDeleteClick = { viewModel.dispatch(LoginViewAction.ClearAccount) }
|
||||
)
|
||||
}
|
||||
item {
|
||||
LoginEditView(
|
||||
text = viewStates.password,
|
||||
labelText = "密码",
|
||||
hintText = "请输入密码",
|
||||
onValueChanged = { viewModel.dispatch(LoginViewAction.UpdatePassword(it)) },
|
||||
onDeleteClick = { viewModel.dispatch(LoginViewAction.ClearPassword) },
|
||||
modifier = Modifier.padding(top = 20.dp, bottom = 20.dp),
|
||||
isPassword = true
|
||||
)
|
||||
}
|
||||
item {
|
||||
AppButton(
|
||||
text = "登录",
|
||||
modifier = Modifier.padding(horizontal = 20.dp)
|
||||
) {
|
||||
keyboardController?.hide()
|
||||
viewModel.dispatch(LoginViewAction.Login)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package com.zj.wanandroid.ui.page.login
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.zj.wanandroid.data.http.HttpResult
|
||||
import com.zj.wanandroid.data.http.HttpService
|
||||
import com.zj.wanandroid.utils.AppUserUtil
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LoginViewModel @Inject constructor(
|
||||
private var service: HttpService
|
||||
) : ViewModel() {
|
||||
var viewStates by mutableStateOf(LoginViewState())
|
||||
private set
|
||||
private val _viewEvents = Channel<LoginViewEvent>(Channel.BUFFERED)
|
||||
val viewEvents = _viewEvents.receiveAsFlow()
|
||||
|
||||
fun dispatch(action: LoginViewAction) {
|
||||
when (action) {
|
||||
is LoginViewAction.Login -> login()
|
||||
is LoginViewAction.ClearAccount -> clearAccount()
|
||||
is LoginViewAction.ClearPassword -> clearPassword()
|
||||
is LoginViewAction.UpdateAccount -> updateAccount(action.account)
|
||||
is LoginViewAction.UpdatePassword -> updatePassword(action.password)
|
||||
}
|
||||
}
|
||||
|
||||
private fun login() {
|
||||
viewModelScope.launch {
|
||||
flow {
|
||||
emit(service.login(viewStates.account.trim(), viewStates.password.trim()))
|
||||
}.map {
|
||||
if (it.errorCode == 0) {
|
||||
if (it.data != null) {
|
||||
HttpResult.Success(it.data!!)
|
||||
} else {
|
||||
throw Exception("the result of remote's request is null")
|
||||
}
|
||||
} else {
|
||||
throw Exception(it.errorMsg)
|
||||
}
|
||||
}.onEach {
|
||||
AppUserUtil.onLogin(it.result)
|
||||
_viewEvents.send(LoginViewEvent.PopBack)
|
||||
}.catch {
|
||||
_viewEvents.send(LoginViewEvent.ErrorMessage(it.message ?: ""))
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearAccount() {
|
||||
viewStates = viewStates.copy(account = "")
|
||||
}
|
||||
|
||||
private fun clearPassword() {
|
||||
viewStates = viewStates.copy(password = "")
|
||||
}
|
||||
|
||||
private fun updateAccount(account: String) {
|
||||
viewStates = viewStates.copy(account = account)
|
||||
}
|
||||
|
||||
private fun updatePassword(password: String) {
|
||||
viewStates = viewStates.copy(password = password)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class LoginViewState(
|
||||
val account: String = "",
|
||||
val password: String = "",
|
||||
val isLogged: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* 一次性事件
|
||||
*/
|
||||
sealed class LoginViewEvent {
|
||||
object PopBack : LoginViewEvent()
|
||||
data class ErrorMessage(val message: String) : LoginViewEvent()
|
||||
}
|
||||
|
||||
sealed class LoginViewAction {
|
||||
object Login : LoginViewAction()
|
||||
object ClearAccount : LoginViewAction()
|
||||
object ClearPassword : LoginViewAction()
|
||||
data class UpdateAccount(val account: String) : LoginViewAction()
|
||||
data class UpdatePassword(val password: String) : LoginViewAction()
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package com.zj.wanandroid.ui.page.main.category
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import com.zj.wanandroid.theme.BottomNavBarHeight
|
||||
import com.zj.wanandroid.ui.page.main.category.navi.NaviPage
|
||||
import com.zj.wanandroid.ui.page.main.category.pubaccount.PublicAccountPage
|
||||
import com.zj.wanandroid.ui.page.main.category.stucture.StructurePage
|
||||
import com.zj.wanandroid.ui.widgets.TextTabBar
|
||||
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun CategoryPage(
|
||||
navCtrl: NavHostController,
|
||||
categoryIndex: Int = 0,
|
||||
viewModel: CategoryViewModel = hiltViewModel()
|
||||
) {
|
||||
|
||||
val titles = viewModel.titles
|
||||
Box(modifier = Modifier.padding(bottom = BottomNavBarHeight)) {
|
||||
Column {
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = categoryIndex,
|
||||
)
|
||||
val scopeState = rememberCoroutineScope()
|
||||
|
||||
Row {
|
||||
TextTabBar(
|
||||
index = pagerState.currentPage,
|
||||
tabTexts = titles,
|
||||
modifier = Modifier.weight(1f),
|
||||
contentAlign = Alignment.CenterStart,
|
||||
onTabSelected = { index ->
|
||||
scopeState.launch {
|
||||
pagerState.scrollToPage(index)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
count = titles.size,
|
||||
state = pagerState,
|
||||
modifier = Modifier.background(AppTheme.colors.background)
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> StructurePage(navCtrl)
|
||||
1 -> NaviPage(navCtrl)
|
||||
2 -> PublicAccountPage(navCtrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.zj.wanandroid.ui.page.main.category
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zj.wanandroid.ui.widgets.TabTitle
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class CategoryViewModel @Inject constructor() : ViewModel() {
|
||||
|
||||
val titles by mutableStateOf(
|
||||
mutableListOf(
|
||||
TabTitle(201, "体系"),
|
||||
TabTitle(202, "导航"),
|
||||
TabTitle(203, "公众号"),
|
||||
)
|
||||
)
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
println("CategoryViewModel ==> onClear")
|
||||
}
|
||||
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package com.zj.wanandroid.ui.page.main.category.navi
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.zj.wanandroid.data.bean.NaviWrapper
|
||||
import com.zj.wanandroid.data.bean.WebData
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import com.zj.wanandroid.ui.widgets.LabelTextButton
|
||||
import com.zj.wanandroid.ui.widgets.LcePage
|
||||
import com.zj.wanandroid.ui.widgets.ListTitle
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
fun NaviPage(navCtrl: NavHostController, viewModel: NaviViewModel = hiltViewModel()) {
|
||||
val viewStates = viewModel.viewStates
|
||||
|
||||
LcePage(pageState = viewStates.pageState, onRetry = {
|
||||
viewModel.dispatch(NaviViewAction.FetchData)
|
||||
}) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = viewStates.listState,
|
||||
contentPadding = PaddingValues(vertical = 10.dp)
|
||||
) {
|
||||
viewStates.dataList.forEachIndexed { index, naviBean ->
|
||||
stickyHeader { ListTitle(title = naviBean.name ?: "标题") }
|
||||
item {
|
||||
NaviItem(naviBean, onSelected = {
|
||||
})
|
||||
if (index <= viewStates.size - 1) {
|
||||
Divider(
|
||||
startIndent = 10.dp,
|
||||
color = AppTheme.colors.divider,
|
||||
thickness = 0.8f.dp
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NaviItem(
|
||||
wrapper: NaviWrapper,
|
||||
isLoading: Boolean = false,
|
||||
onSelected: (WebData) -> Unit = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp)
|
||||
) {
|
||||
if (isLoading) {
|
||||
ListTitle(title = "我是标题")
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(top = 10.dp)
|
||||
) {
|
||||
for (i in 0..7) {
|
||||
LabelTextButton(
|
||||
text = "android",
|
||||
modifier = Modifier.padding(start = 5.dp, bottom = 5.dp),
|
||||
isLoading = true
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
} else {
|
||||
if (!wrapper.articles.isNullOrEmpty()) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(top = 10.dp)
|
||||
) {
|
||||
for (item in wrapper.articles!!) {
|
||||
LabelTextButton(
|
||||
text = item.title ?: "android",
|
||||
modifier = Modifier.padding(start = 5.dp, bottom = 5.dp),
|
||||
onClick = {
|
||||
val webData = WebData(item.title, item.link!!)
|
||||
onSelected(webData)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.zj.wanandroid.ui.page.main.category.navi
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.zj.wanandroid.data.bean.NaviWrapper
|
||||
import com.zj.wanandroid.data.http.HttpService
|
||||
import com.zj.wanandroid.data.http.PageState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NaviViewModel @Inject constructor(
|
||||
private var service: HttpService
|
||||
) : ViewModel() {
|
||||
var viewStates by mutableStateOf(NaviViewState())
|
||||
private set
|
||||
|
||||
init {
|
||||
dispatch(NaviViewAction.FetchData)
|
||||
}
|
||||
|
||||
fun dispatch(action: NaviViewAction) {
|
||||
when (action) {
|
||||
is NaviViewAction.FetchData -> fetchData()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchData() {
|
||||
viewModelScope.launch {
|
||||
flow {
|
||||
emit(service.getNavigationList())
|
||||
}.map {
|
||||
it.data ?: emptyList()
|
||||
}.onStart {
|
||||
viewStates = viewStates.copy(pageState = PageState.Loading)
|
||||
}.onEach {
|
||||
viewStates = viewStates.copy(
|
||||
dataList = it,
|
||||
pageState = PageState.Success(it.isEmpty())
|
||||
)
|
||||
}.catch {
|
||||
viewStates = viewStates.copy(pageState = PageState.Error(it))
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class NaviViewState(
|
||||
val dataList: List<NaviWrapper> = emptyList(),
|
||||
val pageState: PageState = PageState.Loading,
|
||||
val listState: LazyListState = LazyListState()
|
||||
) {
|
||||
val size = dataList.size
|
||||
}
|
||||
|
||||
sealed class NaviViewAction() {
|
||||
object FetchData : NaviViewAction()
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.zj.wanandroid.ui.page.main.category.pubaccount
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.zj.wanandroid.data.bean.ParentBean
|
||||
import com.zj.wanandroid.data.http.HttpService
|
||||
import com.zj.wanandroid.data.http.PageState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class PubAccountViewModel @Inject constructor(
|
||||
private var service: HttpService
|
||||
) : ViewModel() {
|
||||
var viewStates by mutableStateOf(PubAccountViewState())
|
||||
private set
|
||||
|
||||
init {
|
||||
dispatch(PubAccountViewAction.FetchData)
|
||||
}
|
||||
|
||||
fun dispatch(action: PubAccountViewAction) {
|
||||
when (action) {
|
||||
is PubAccountViewAction.FetchData -> fetchData()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchData() {
|
||||
viewModelScope.launch {
|
||||
flow {
|
||||
emit(service.getPublicInformation())
|
||||
}.map {
|
||||
it.data ?: emptyList()
|
||||
}.onStart {
|
||||
viewStates = viewStates.copy(pageState = PageState.Loading)
|
||||
}.onEach {
|
||||
viewStates = viewStates.copy(
|
||||
dataList = it,
|
||||
pageState = PageState.Success(it.isEmpty())
|
||||
)
|
||||
}.catch {
|
||||
viewStates = viewStates.copy(pageState = PageState.Error(it))
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class PubAccountViewState(
|
||||
val dataList: List<ParentBean> = emptyList(),
|
||||
val pageState: PageState = PageState.Loading,
|
||||
val listState: LazyListState = LazyListState()
|
||||
) {
|
||||
val size = dataList.size
|
||||
}
|
||||
|
||||
sealed class PubAccountViewAction() {
|
||||
object FetchData : PubAccountViewAction()
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package com.zj.wanandroid.ui.page.main.category.pubaccount
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.GridCells
|
||||
import androidx.compose.foundation.lazy.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import com.zj.wanandroid.data.bean.ParentBean
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import com.zj.wanandroid.theme.white1
|
||||
import com.zj.wanandroid.ui.widgets.LcePage
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
fun PublicAccountPage(
|
||||
navCtrl: NavHostController,
|
||||
viewModel: PubAccountViewModel = hiltViewModel()
|
||||
) {
|
||||
val viewStates = viewModel.viewStates
|
||||
|
||||
LcePage(pageState = viewStates.pageState, onRetry = {
|
||||
viewModel.dispatch(PubAccountViewAction.FetchData)
|
||||
}) {
|
||||
LazyVerticalGrid(
|
||||
cells = GridCells.Fixed(2),
|
||||
modifier = Modifier
|
||||
.background(AppTheme.colors.background)
|
||||
.wrapContentHeight()
|
||||
.padding(10.dp),
|
||||
state = viewStates.listState
|
||||
) {
|
||||
itemsIndexed(viewStates.dataList) { index, item ->
|
||||
Box(Modifier.padding(vertical = 5.dp)) {
|
||||
when (index % 4) {
|
||||
0 -> PublicAccountItem(
|
||||
parent = item,
|
||||
click = {},
|
||||
isPrimary = true,
|
||||
roundedCorner = RoundedCornerShape(topStart = 5.dp, bottomStart = 5.dp)
|
||||
)
|
||||
1 -> PublicAccountItem(
|
||||
parent = item,
|
||||
click = {},
|
||||
isPrimary = false,
|
||||
roundedCorner = RoundedCornerShape(topEnd = 5.dp, bottomEnd = 5.dp)
|
||||
)
|
||||
2 -> PublicAccountItem(
|
||||
parent = item,
|
||||
click = {},
|
||||
isPrimary = false,
|
||||
roundedCorner = RoundedCornerShape(topStart = 5.dp, bottomStart = 5.dp)
|
||||
)
|
||||
3 -> PublicAccountItem(
|
||||
parent = item,
|
||||
click = {},
|
||||
isPrimary = true,
|
||||
roundedCorner = RoundedCornerShape(topEnd = 5.dp, bottomEnd = 5.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SpecialText(
|
||||
parent: ParentBean,
|
||||
textColor: Color,
|
||||
bgColor: Color,
|
||||
shape: RoundedCornerShape,
|
||||
onClick: (ParentBean) -> Unit
|
||||
) {
|
||||
Text(
|
||||
text = parent.name!!,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 18.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(80.dp)
|
||||
.background(
|
||||
color = bgColor,
|
||||
shape = shape
|
||||
)
|
||||
.padding(top = 30.dp)
|
||||
.clickable {
|
||||
onClick(parent)
|
||||
},
|
||||
fontWeight = FontWeight.W500,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PublicAccountItem(
|
||||
parent: ParentBean,
|
||||
click: (ParentBean) -> Unit,
|
||||
isPrimary: Boolean = true,
|
||||
roundedCorner: RoundedCornerShape = RoundedCornerShape(5.dp)
|
||||
) {
|
||||
SpecialText(
|
||||
parent = parent,
|
||||
textColor = if (isPrimary) white1 else AppTheme.colors.textSecondary,
|
||||
bgColor = if (isPrimary) AppTheme.colors.themeUi else white1,
|
||||
shape = roundedCorner
|
||||
) {
|
||||
click(it)
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package com.zj.wanandroid.ui.page.main.category.stucture
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.zj.wanandroid.data.bean.ParentBean
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import com.zj.wanandroid.ui.page.common.RouteName
|
||||
import com.zj.wanandroid.ui.widgets.LabelTextButton
|
||||
import com.zj.wanandroid.ui.widgets.LcePage
|
||||
import com.zj.wanandroid.ui.widgets.ListTitle
|
||||
import com.zj.wanandroid.utils.RouteUtils
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
fun StructurePage(
|
||||
navCtrl: NavHostController,
|
||||
viewModel: StructureViewModel = hiltViewModel()
|
||||
) {
|
||||
val viewStates = viewModel.viewStates
|
||||
|
||||
LcePage(pageState = viewStates.pageState, onRetry = {
|
||||
viewModel.dispatch(StructureViewAction.FetchData)
|
||||
}) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.background(AppTheme.colors.background),
|
||||
state = viewStates.listState,
|
||||
contentPadding = PaddingValues(vertical = 10.dp)
|
||||
) {
|
||||
viewStates.dataList.forEachIndexed { position, chapter1 ->
|
||||
stickyHeader { ListTitle(title = chapter1.name ?: "标题") }
|
||||
item {
|
||||
StructureItem(chapter1, onSelect = { parent ->
|
||||
|
||||
})
|
||||
if (position <= viewStates.size - 1) {
|
||||
Divider(
|
||||
startIndent = 10.dp,
|
||||
color = AppTheme.colors.divider,
|
||||
thickness = 0.8f.dp
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StructureItem(
|
||||
bean: ParentBean,
|
||||
isLoading: Boolean = false,
|
||||
onSelect: (parent: ParentBean) -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp)
|
||||
) {
|
||||
if (isLoading) {
|
||||
ListTitle(title = "我都标题", isLoading = true)
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(horizontal = 10.dp)
|
||||
) {
|
||||
for (i in 0..7) {
|
||||
LabelTextButton(
|
||||
text = "android",
|
||||
modifier = Modifier.padding(start = 5.dp, bottom = 5.dp),
|
||||
isLoading = true
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
} else {
|
||||
if (!bean.children.isNullOrEmpty()) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(horizontal = 10.dp)
|
||||
) {
|
||||
for (item in bean.children!!) {
|
||||
LabelTextButton(
|
||||
text = item.name ?: "android",
|
||||
modifier = Modifier.padding(start = 5.dp, bottom = 5.dp),
|
||||
onClick = {
|
||||
onSelect(item)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.zj.wanandroid.ui.page.main.category.stucture
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.zj.wanandroid.data.bean.ParentBean
|
||||
import com.zj.wanandroid.data.http.HttpService
|
||||
import com.zj.wanandroid.data.http.PageState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class StructureViewModel @Inject constructor(
|
||||
private var service: HttpService
|
||||
) : ViewModel() {
|
||||
var viewStates by mutableStateOf(StructureViewState())
|
||||
private set
|
||||
|
||||
init {
|
||||
dispatch(StructureViewAction.FetchData)
|
||||
}
|
||||
|
||||
fun dispatch(action: StructureViewAction) {
|
||||
when (action) {
|
||||
is StructureViewAction.FetchData -> fetchData()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchData() {
|
||||
viewModelScope.launch {
|
||||
flow {
|
||||
emit(service.getStructureList())
|
||||
}.map {
|
||||
it.data ?: emptyList()
|
||||
}.onStart {
|
||||
viewStates = viewStates.copy(pageState = PageState.Loading)
|
||||
}.onEach {
|
||||
viewStates = viewStates.copy(
|
||||
dataList = it,
|
||||
pageState = PageState.Success(it.isEmpty())
|
||||
)
|
||||
}.catch {
|
||||
viewStates = viewStates.copy(pageState = PageState.Error(it))
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class StructureViewState(
|
||||
val dataList: List<ParentBean> = emptyList(),
|
||||
val pageState: PageState = PageState.Loading,
|
||||
val listState: LazyListState = LazyListState()
|
||||
) {
|
||||
val size = dataList.size
|
||||
}
|
||||
|
||||
sealed class StructureViewAction() {
|
||||
object FetchData : StructureViewAction()
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package com.zj.wanandroid.ui.page.main.collect
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Face
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.zj.wanandroid.data.bean.WebData
|
||||
import com.zj.wanandroid.theme.BottomNavBarHeight
|
||||
import com.zj.wanandroid.ui.page.common.RouteName
|
||||
import com.zj.wanandroid.ui.widgets.*
|
||||
import com.zj.wanandroid.utils.RouteUtils
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
fun CollectPage(
|
||||
navCtrl: NavHostController,
|
||||
scaffoldState: ScaffoldState,
|
||||
viewModel: CollectViewModel = hiltViewModel()
|
||||
) {
|
||||
val viewStates = viewModel.viewStates
|
||||
val collectPaging = viewStates.pagingData?.collectAsLazyPagingItems()
|
||||
val webUrls = viewStates.urlList
|
||||
val titles = viewStates.titles
|
||||
val isRefreshing = viewStates.isRefreshing
|
||||
val listState =
|
||||
if ((collectPaging?.itemCount ?: 0) > 0) viewStates.listState else LazyListState()
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
Log.i("debug", "onStart")
|
||||
viewModel.dispatch(CollectViewAction.OnStart)
|
||||
onDispose {
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = BottomNavBarHeight)
|
||||
) {
|
||||
AppToolsBar(title = "我的收藏")
|
||||
|
||||
if (!viewStates.isLogged) {
|
||||
EmptyView(
|
||||
tips = "点击登录",
|
||||
imageVector = Icons.Default.Face
|
||||
) {
|
||||
RouteUtils.navTo(navCtrl, RouteName.LOGIN)
|
||||
}
|
||||
} else {
|
||||
collectPaging?.let {
|
||||
RefreshList(it, listState = listState, isRefreshing = isRefreshing, onRefresh = {
|
||||
viewModel.dispatch(CollectViewAction.Refresh)
|
||||
}) {
|
||||
if (!webUrls.isNullOrEmpty()) {
|
||||
stickyHeader {
|
||||
ListTitle(title = titles[1].text)
|
||||
}
|
||||
item {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(10.dp)
|
||||
) {
|
||||
webUrls?.forEachIndexed { index, website ->
|
||||
LabelTextButton(
|
||||
text = website.name ?: "标签",
|
||||
modifier = Modifier.padding(end = 10.dp, bottom = 10.dp),
|
||||
onClick = {
|
||||
RouteUtils.navTo(
|
||||
navCtrl,
|
||||
RouteName.WEB_VIEW,
|
||||
WebData(website.name, website.link!!)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stickyHeader {
|
||||
ListTitle(title = titles[0].text)
|
||||
}
|
||||
if (collectPaging.itemCount > 0) {
|
||||
items(collectPaging) { collectItem ->
|
||||
CollectListItemView(
|
||||
collectItem!!,
|
||||
onClick = {
|
||||
RouteUtils.navTo(
|
||||
navCtrl,
|
||||
RouteName.WEB_VIEW,
|
||||
WebData(collectItem.title, collectItem.link)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package com.zj.wanandroid.ui.page.main.collect
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import com.zj.wanandroid.common.paging.simplePager
|
||||
import com.zj.wanandroid.data.bean.CollectBean
|
||||
import com.zj.wanandroid.data.bean.ParentBean
|
||||
import com.zj.wanandroid.data.http.HttpService
|
||||
import com.zj.wanandroid.ui.widgets.TabTitle
|
||||
import com.zj.wanandroid.utils.AppUserUtil
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class CollectViewModel @Inject constructor(
|
||||
private var service: HttpService
|
||||
) : ViewModel() {
|
||||
private val pager by lazy {
|
||||
simplePager {
|
||||
service.getCollectionList(it)
|
||||
}.cachedIn(viewModelScope)
|
||||
}
|
||||
var viewStates by mutableStateOf(CollectViewState())
|
||||
private set
|
||||
|
||||
fun dispatch(action: CollectViewAction) {
|
||||
when (action) {
|
||||
is CollectViewAction.OnStart -> onStart()
|
||||
is CollectViewAction.Refresh -> refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onStart() {
|
||||
viewStates = viewStates.copy(isLogged = AppUserUtil.isLogged)
|
||||
if (viewStates.isLogged && viewStates.isInit.not()) {
|
||||
viewStates = viewStates.copy(isInit = true)
|
||||
initData()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initData() {
|
||||
viewStates = viewStates.copy(pagingData = pager)
|
||||
fetchUrls()
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
fetchUrls()
|
||||
}
|
||||
|
||||
private fun fetchUrls() {
|
||||
viewModelScope.launch {
|
||||
flow {
|
||||
emit(service.getCollectUrls())
|
||||
}.onStart {
|
||||
viewStates = viewStates.copy(isRefreshing = true)
|
||||
}.map {
|
||||
it.data ?: emptyList()
|
||||
}.onEach {
|
||||
viewStates = viewStates.copy(urlList = it, isRefreshing = false)
|
||||
}.catch {
|
||||
viewStates = viewStates.copy(isRefreshing = false)
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class CollectViewState(
|
||||
val isInit: Boolean = false,
|
||||
val isRefreshing: Boolean = false,
|
||||
val listState: LazyListState = LazyListState(),
|
||||
val urlList: List<ParentBean> = emptyList(),
|
||||
val pagingData: PagingCollect? = null,
|
||||
val isLogged: Boolean = AppUserUtil.isLogged,
|
||||
val titles: List<TabTitle> = listOf(
|
||||
TabTitle(301, "文章列表"),
|
||||
TabTitle(302, "我的网址"),
|
||||
)
|
||||
)
|
||||
|
||||
sealed class CollectViewAction {
|
||||
object OnStart : CollectViewAction()
|
||||
object Refresh : CollectViewAction()
|
||||
}
|
||||
|
||||
typealias PagingCollect = Flow<PagingData<CollectBean>>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.zj.wanandroid.ui.page.main.home
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.zj.wanandroid.ui.page.common.RouteName
|
||||
import com.zj.wanandroid.ui.page.main.home.question.QuestionPage
|
||||
import com.zj.wanandroid.ui.page.main.home.recommend.RecommendPage
|
||||
import com.zj.wanandroid.ui.page.main.home.square.SquarePage
|
||||
import com.zj.wanandroid.ui.widgets.HomeSearchBar
|
||||
import com.zj.wanandroid.ui.widgets.TextTabBar
|
||||
import com.zj.wanandroid.utils.RouteUtils
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ExperimentalPagerApi
|
||||
@Composable
|
||||
fun HomePage(
|
||||
navCtrl: NavHostController,
|
||||
scaffoldState: ScaffoldState,
|
||||
viewModel: HomeViewModel = hiltViewModel()
|
||||
) {
|
||||
val titles = viewModel.viewStates.titles
|
||||
val scopeState = rememberCoroutineScope()
|
||||
Column {
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = 0
|
||||
)
|
||||
|
||||
TextTabBar(
|
||||
index = pagerState.currentPage,
|
||||
tabTexts = titles,
|
||||
onTabSelected = { index ->
|
||||
scopeState.launch {
|
||||
pagerState.scrollToPage(index)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
HomeSearchBar(
|
||||
onSearchClick = {
|
||||
RouteUtils.navTo(navCtrl, RouteName.ARTICLE_SEARCH + "/111")
|
||||
}
|
||||
)
|
||||
|
||||
HorizontalPager(
|
||||
count = titles.size,
|
||||
state = pagerState,
|
||||
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 50.dp)
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> SquarePage(navCtrl, scaffoldState)
|
||||
1 -> RecommendPage(navCtrl, scaffoldState)
|
||||
2 -> QuestionPage(navCtrl, scaffoldState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.zj.wanandroid.ui.page.main.home
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zj.wanandroid.ui.widgets.TabTitle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor() : ViewModel() {
|
||||
var viewStates by mutableStateOf(HomeViewState())
|
||||
private set
|
||||
|
||||
init {
|
||||
viewStates = viewStates.copy(
|
||||
titles = listOf(
|
||||
TabTitle(101, "广场"),
|
||||
TabTitle(102, "推荐"),
|
||||
TabTitle(103, "问答")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class HomeViewState(val titles: List<TabTitle> = emptyList())
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package com.zj.wanandroid.ui.page.main.home.question
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemsIndexed
|
||||
import com.zj.wanandroid.data.bean.Article
|
||||
import com.zj.wanandroid.data.bean.WebData
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import com.zj.wanandroid.ui.page.common.RouteName
|
||||
import com.zj.wanandroid.ui.widgets.MainTitle
|
||||
import com.zj.wanandroid.ui.widgets.MiniTitle
|
||||
import com.zj.wanandroid.ui.widgets.RefreshList
|
||||
import com.zj.wanandroid.ui.widgets.TextContent
|
||||
import com.zj.wanandroid.utils.RegexUtils
|
||||
import com.zj.wanandroid.utils.RouteUtils
|
||||
|
||||
@Composable
|
||||
fun QuestionPage(
|
||||
navCtrl: NavHostController,
|
||||
scaffoldState: ScaffoldState,
|
||||
viewModel: QuestionViewModel = hiltViewModel()
|
||||
) {
|
||||
val viewStates = viewModel.viewStates
|
||||
val questionData = viewStates.pagingData.collectAsLazyPagingItems()
|
||||
val listState = if (questionData.itemCount > 0) viewStates.listState else LazyListState()
|
||||
|
||||
RefreshList(questionData,listState = listState) {
|
||||
itemsIndexed(questionData) { _, item ->
|
||||
WenDaItem(item!!) {
|
||||
item.run {
|
||||
RouteUtils.navTo(
|
||||
navCtrl,
|
||||
RouteName.WEB_VIEW,
|
||||
WebData(title, link!!)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WenDaItem(article: Article, isLoading: Boolean = false, onClick: () -> Unit = {}) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(5.dp)
|
||||
.clickable(enabled = !isLoading) {
|
||||
onClick.invoke()
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp)
|
||||
) {
|
||||
|
||||
//标题
|
||||
MainTitle(
|
||||
title = titleSubstring(article.title) ?: "每日一问",
|
||||
maxLine = 2,
|
||||
isLoading = isLoading,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.CenterHorizontally)
|
||||
.padding(vertical = 5.dp)
|
||||
) {
|
||||
|
||||
//UserIcon(isLoading = isLoading)
|
||||
MiniTitle(
|
||||
text = "作者:${article.author ?: "xxx"}",
|
||||
color = AppTheme.colors.textSecondary,
|
||||
modifier = Modifier
|
||||
.padding(start = if (isLoading) 5.dp else 0.dp)
|
||||
.align(Alignment.CenterVertically),
|
||||
isLoading = isLoading
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
//发布时间
|
||||
//TimerIcon(isLoading = isLoading)
|
||||
MiniTitle(
|
||||
modifier = Modifier
|
||||
.padding(start = if (isLoading) 5.dp else 0.dp)
|
||||
.align(Alignment.CenterVertically),
|
||||
text = "日期:${RegexUtils().timestamp(article.niceDate) ?: "2020"}",
|
||||
color = AppTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
|
||||
TextContent(
|
||||
text = RegexUtils().symbolClear(article.desc),
|
||||
maxLines = 3,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 60.dp),
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun titleSubstring(oldText: String?): String? {
|
||||
return oldText?.run {
|
||||
var newText = this
|
||||
if (startsWith("每日一问") && contains(" | ")) {
|
||||
newText = substring(indexOf(" | ") + 3, length)
|
||||
}
|
||||
newText
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.zj.wanandroid.ui.page.main.home.question
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zj.wanandroid.common.paging.simplePager
|
||||
import com.zj.wanandroid.data.http.HttpService
|
||||
import com.zj.wanandroid.ui.page.main.home.square.PagingArticle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class QuestionViewModel @Inject constructor(
|
||||
private var service: HttpService,
|
||||
) : ViewModel() {
|
||||
private val pager by lazy {
|
||||
simplePager {
|
||||
service.getWendaData(it)
|
||||
}
|
||||
}
|
||||
var viewStates by mutableStateOf(QuestionViewState(pagingData = pager))
|
||||
private set
|
||||
}
|
||||
|
||||
data class QuestionViewState(
|
||||
val isRefreshing: Boolean = false,
|
||||
val listState: LazyListState = LazyListState(),
|
||||
val pagingData: PagingArticle
|
||||
)
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.zj.wanandroid.ui.page.main.home.recommend
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemsIndexed
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.zj.wanandroid.data.bean.WebData
|
||||
import com.zj.wanandroid.ui.page.common.RouteName
|
||||
import com.zj.wanandroid.ui.widgets.Banner
|
||||
import com.zj.wanandroid.ui.widgets.MultiStateItemView
|
||||
import com.zj.wanandroid.ui.widgets.RefreshList
|
||||
import com.zj.wanandroid.utils.RouteUtils
|
||||
|
||||
|
||||
@ExperimentalPagerApi
|
||||
@Composable
|
||||
fun RecommendPage(
|
||||
navCtrl: NavHostController,
|
||||
scaffoldState: ScaffoldState,
|
||||
viewModel: RecommendViewModel = hiltViewModel()
|
||||
) {
|
||||
val viewStates = viewModel.viewStates
|
||||
val recommendData = viewStates.pagingData.collectAsLazyPagingItems()
|
||||
val banners = viewStates.imageList
|
||||
val topArticle = viewStates.topArticles
|
||||
val isRefreshing = viewStates.isRefreshing
|
||||
val listState = if (recommendData.itemCount > 0) viewStates.listState else LazyListState()
|
||||
|
||||
RefreshList(recommendData, listState = listState, isRefreshing = isRefreshing, onRefresh = {
|
||||
viewModel.dispatch(RecommendViewAction.Refresh)
|
||||
}) {
|
||||
if (banners.isNotEmpty()) {
|
||||
item {
|
||||
Banner(list = banners) { url, title ->
|
||||
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, WebData(title, url))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (topArticle.isNotEmpty()) {
|
||||
itemsIndexed(topArticle) { index, item ->
|
||||
MultiStateItemView(
|
||||
modifier = Modifier.padding(top = if (index == 0) 5.dp else 0.dp),
|
||||
data = item,
|
||||
isTop = true,
|
||||
onSelected = { data ->
|
||||
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, data)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(recommendData) { _, item ->
|
||||
MultiStateItemView(
|
||||
data = item!!,
|
||||
onSelected = {
|
||||
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, it)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.zj.wanandroid.ui.page.main.home.recommend
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import com.zj.wanandroid.common.paging.simplePager
|
||||
import com.zj.wanandroid.data.bean.Article
|
||||
import com.zj.wanandroid.data.http.HttpService
|
||||
import com.zj.wanandroid.ui.page.main.home.square.PagingArticle
|
||||
import com.zj.wanandroid.ui.widgets.BannerData
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class RecommendViewModel @Inject constructor(
|
||||
private var service: HttpService,
|
||||
) : ViewModel() {
|
||||
private val pager by lazy {
|
||||
simplePager {
|
||||
service.getIndexList(it)
|
||||
}.cachedIn(viewModelScope)
|
||||
}
|
||||
var viewStates by mutableStateOf(RecommendViewState(pagingData = pager))
|
||||
private set
|
||||
|
||||
init {
|
||||
dispatch(RecommendViewAction.FetchData)
|
||||
}
|
||||
|
||||
fun dispatch(action: RecommendViewAction) {
|
||||
when (action) {
|
||||
is RecommendViewAction.FetchData -> fetchData()
|
||||
is RecommendViewAction.Refresh -> refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchData() {
|
||||
val imageListFlow = flow {
|
||||
kotlinx.coroutines.delay(2000)
|
||||
emit(service.getBanners())
|
||||
}.map { bean ->
|
||||
val result = mutableListOf<BannerData>()
|
||||
bean.data?.forEach {
|
||||
result.add(BannerData(it.title ?: "", it.imagePath ?: "", it.url ?: ""))
|
||||
}
|
||||
result
|
||||
}
|
||||
val topListFlow = flow {
|
||||
emit(service.getTopArticles())
|
||||
}.map {
|
||||
it.data ?: emptyList()
|
||||
}
|
||||
viewModelScope.launch {
|
||||
imageListFlow.zip(topListFlow) { banners, tops ->
|
||||
viewStates =
|
||||
viewStates.copy(imageList = banners, topArticles = tops, isRefreshing = false)
|
||||
}.onStart {
|
||||
viewStates = viewStates.copy(isRefreshing = true)
|
||||
}.catch {
|
||||
viewStates = viewStates.copy(isRefreshing = false)
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class RecommendViewState(
|
||||
val pagingData: PagingArticle,
|
||||
val isRefreshing: Boolean = false,
|
||||
val imageList: List<BannerData> = emptyList(),
|
||||
val topArticles: List<Article> = emptyList(),
|
||||
val listState: LazyListState = LazyListState()
|
||||
)
|
||||
|
||||
sealed class RecommendViewAction {
|
||||
object FetchData : RecommendViewAction()
|
||||
object Refresh : RecommendViewAction()
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.zj.wanandroid.ui.page.main.home.square
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemsIndexed
|
||||
import com.zj.wanandroid.ui.page.common.RouteName
|
||||
import com.zj.wanandroid.ui.widgets.MultiStateItemView
|
||||
import com.zj.wanandroid.ui.widgets.RefreshList
|
||||
import com.zj.wanandroid.utils.RouteUtils
|
||||
|
||||
@Composable
|
||||
fun SquarePage(
|
||||
navCtrl: NavHostController,
|
||||
scaffoldState: ScaffoldState,
|
||||
viewModel: SquareViewModel = hiltViewModel()
|
||||
) {
|
||||
val viewStates = remember { viewModel.viewStates }
|
||||
val squareData = viewStates.pagingData.collectAsLazyPagingItems()
|
||||
val listState = if (squareData.itemCount > 0) viewStates.listState else LazyListState()
|
||||
|
||||
RefreshList(squareData, listState = listState) {
|
||||
itemsIndexed(squareData) { _, item ->
|
||||
MultiStateItemView(
|
||||
data = item!!,
|
||||
onSelected = {
|
||||
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, it)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.zj.wanandroid.ui.page.main.home.square
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import com.zj.wanandroid.common.paging.simplePager
|
||||
import com.zj.wanandroid.data.bean.Article
|
||||
import com.zj.wanandroid.data.http.HttpService
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SquareViewModel @Inject constructor(
|
||||
private var service: HttpService,
|
||||
) : ViewModel() {
|
||||
private val pager by lazy {
|
||||
simplePager {
|
||||
if (it >= 3) {
|
||||
throw NullPointerException("")
|
||||
}
|
||||
delay(2000)
|
||||
service.getSquareData(it)
|
||||
}.cachedIn(viewModelScope)
|
||||
}
|
||||
var viewStates by mutableStateOf(SquareViewState(pagingData = pager))
|
||||
private set
|
||||
}
|
||||
|
||||
data class SquareViewState(
|
||||
val isRefreshing: Boolean = false,
|
||||
val listState: LazyListState = LazyListState(),
|
||||
val pagingData: PagingArticle
|
||||
)
|
||||
|
||||
sealed class SquareViewAction {
|
||||
object Refresh : SquareViewAction()
|
||||
}
|
||||
|
||||
typealias PagingArticle = Flow<PagingData<Article>>
|
||||
+417
@@ -0,0 +1,417 @@
|
||||
package com.zj.wanandroid.ui.page.main.profile
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Face
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import com.google.accompanist.placeholder.material.placeholder
|
||||
import com.zj.wanandroid.R
|
||||
import com.zj.wanandroid.data.bean.PointsBean
|
||||
import com.zj.wanandroid.data.bean.UserInfo
|
||||
import com.zj.wanandroid.data.bean.WebData
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import com.zj.wanandroid.theme.BottomNavBarHeight
|
||||
import com.zj.wanandroid.theme.ToolBarHeight
|
||||
import com.zj.wanandroid.theme.white
|
||||
import com.zj.wanandroid.ui.page.common.RouteName
|
||||
import com.zj.wanandroid.ui.widgets.*
|
||||
import com.zj.wanandroid.utils.RouteUtils
|
||||
|
||||
@Composable
|
||||
fun ProfilePage(
|
||||
navCtrl: NavHostController,
|
||||
scaffoldState: ScaffoldState,
|
||||
viewModel: ProfileViewModel = hiltViewModel()
|
||||
) {
|
||||
val viewStates = viewModel.viewStates
|
||||
val isLogged = viewStates.isLogged
|
||||
val userInfo = viewStates.userInfo
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
Log.i("debug", "onStart")
|
||||
viewModel.dispatch(ProfileViewAction.OnStart)
|
||||
onDispose {
|
||||
}
|
||||
}
|
||||
|
||||
if (viewStates.showLogout) {
|
||||
SampleAlertDialog(
|
||||
title = "提示",
|
||||
content = "退出后,将无法查看我的文章、消息、收藏、积分、浏览记录等功能,确定退出登录吗?",
|
||||
onConfirmClick = {
|
||||
viewModel.dispatch(ProfileViewAction.Logout)
|
||||
},
|
||||
onDismiss = {
|
||||
viewModel.dispatch(ProfileViewAction.DismissLogoutDialog)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(bottom = BottomNavBarHeight)
|
||||
.fillMaxSize()
|
||||
.background(color = AppTheme.colors.mainColor)
|
||||
) {
|
||||
if (isLogged && userInfo != null) {
|
||||
LazyColumn() {
|
||||
item {
|
||||
HeaderPart(
|
||||
navCtrl = navCtrl,
|
||||
userInfo = userInfo,
|
||||
messageCount = 0
|
||||
)
|
||||
}
|
||||
item {
|
||||
FooterPart(
|
||||
navCtrl = navCtrl,
|
||||
messageCount = 0,
|
||||
onLogout = {
|
||||
viewModel.dispatch(ProfileViewAction.ShowLogoutDialog)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AppToolsBar(title = "我的")
|
||||
EmptyView(
|
||||
tips = "点击登录",
|
||||
imageVector = Icons.Default.Face
|
||||
) {
|
||||
RouteUtils.navTo(navCtrl, RouteName.LOGIN)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//基本操作
|
||||
@Composable
|
||||
fun FooterPart(
|
||||
navCtrl: NavHostController,
|
||||
messageCount: Int,
|
||||
onLogout: () -> Unit,
|
||||
) {
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(bottom = 10.dp)
|
||||
) {
|
||||
ArrowRightListItem(
|
||||
iconRes = painterResource(R.drawable.ic_message),
|
||||
title = "消息",
|
||||
msgCount = messageCount
|
||||
) {
|
||||
//TODO
|
||||
}
|
||||
ArrowRightListItem(
|
||||
iconRes = painterResource(R.drawable.ic_menu_settings),
|
||||
title = "设置"
|
||||
) {
|
||||
onLogout.invoke()
|
||||
}
|
||||
ArrowRightListItem(
|
||||
iconRes = painterResource(R.drawable.ic_feedback),
|
||||
title = "WanAndroid"
|
||||
) {
|
||||
RouteUtils.navTo(
|
||||
navCtrl = navCtrl,
|
||||
destinationName = RouteName.WEB_VIEW,
|
||||
args = WebData(title = "官方网站", url = "https://www.wanandroid.com/index")
|
||||
)
|
||||
}
|
||||
ArrowRightListItem(
|
||||
iconRes = painterResource(R.drawable.ic_data),
|
||||
title = "积分规则"
|
||||
) {
|
||||
RouteUtils.navTo(
|
||||
navCtrl = navCtrl,
|
||||
destinationName = RouteName.WEB_VIEW,
|
||||
args = WebData(title = "积分规则", url = "https://www.wanandroid.com/blog/show/2653")
|
||||
)
|
||||
}
|
||||
ArrowRightListItem(
|
||||
iconRes = painterResource(R.drawable.ic_message),
|
||||
title = "退出登录"
|
||||
) {
|
||||
onLogout.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//用户信息
|
||||
@Composable
|
||||
fun HeaderPart(
|
||||
navCtrl: NavHostController,
|
||||
userInfo: UserInfo,
|
||||
messageCount: Int,
|
||||
) {
|
||||
Column {
|
||||
ProfileToolBar(
|
||||
msgCount = messageCount,
|
||||
onMessageIconClick = {
|
||||
//TODO
|
||||
},
|
||||
onDeleteIconClick = {
|
||||
//TODO
|
||||
},
|
||||
onDashboardIconClick = {
|
||||
//TODO
|
||||
},
|
||||
)
|
||||
UserInfoItem(null, userInfo)
|
||||
UserOptionsItem(
|
||||
onCollectClick = {
|
||||
try {
|
||||
navCtrl.navigate(RouteName.COLLECTION) {
|
||||
popUpTo(navCtrl.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
ex.printStackTrace()
|
||||
}
|
||||
},
|
||||
onMyShareClick = {
|
||||
//TODO
|
||||
},
|
||||
onHistoryClick = {
|
||||
//TODO
|
||||
},
|
||||
onRankingClick = {
|
||||
//TODO
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//调色板、清理缓存、消息的ToolBar
|
||||
@Composable
|
||||
private fun ProfileToolBar(
|
||||
msgCount: Int,
|
||||
onMessageIconClick: () -> Unit,
|
||||
onDeleteIconClick: () -> Unit,
|
||||
onDashboardIconClick: () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(ToolBarHeight)
|
||||
.background(AppTheme.colors.themeUi)
|
||||
) {
|
||||
Row(modifier = Modifier.align(Alignment.CenterEnd)) {
|
||||
Box(
|
||||
modifier = Modifier.wrapContentSize()
|
||||
) {
|
||||
NotificationIcon(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable { onMessageIconClick.invoke() }
|
||||
)
|
||||
if (msgCount > 0) {
|
||||
DotView(modifier = Modifier.align(Alignment.TopEnd))
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Clear cache",
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.clickable {
|
||||
onDeleteIconClick.invoke()
|
||||
},
|
||||
tint = white
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_theme),
|
||||
contentDescription = "Switch theme",
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.clickable {
|
||||
onDashboardIconClick.invoke()
|
||||
},
|
||||
tint = white
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//用户基本信息
|
||||
@Composable
|
||||
private fun UserInfoItem(myPoints: PointsBean?, userInfo: UserInfo?) {
|
||||
|
||||
ConstraintLayout(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.background(AppTheme.colors.themeUi)
|
||||
) {
|
||||
|
||||
val (icon, info) = createRefs()
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.ic_account),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.padding(start = 20.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.placeholder(
|
||||
visible = userInfo == null,
|
||||
color = AppTheme.colors.placeholder
|
||||
)
|
||||
.constrainAs(icon) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(parent.start)
|
||||
},
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp, bottom = 20.dp)
|
||||
.constrainAs(info) {
|
||||
start.linkTo(icon.end)
|
||||
top.linkTo(parent.top)
|
||||
}
|
||||
) {
|
||||
MainTitle(
|
||||
title = userInfo?.username ?: "",
|
||||
modifier = Modifier.placeholder(
|
||||
visible = userInfo == null,
|
||||
color = AppTheme.colors.placeholder
|
||||
)
|
||||
)
|
||||
if (userInfo?.email?.isNotEmpty() == true) {
|
||||
MiniTitle(
|
||||
text = "email: ${userInfo.email}",
|
||||
modifier = Modifier.padding(top = 5.dp)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 5.dp)
|
||||
) {
|
||||
TagView(
|
||||
tagText = "Lv${myPoints?.level ?: "99"}",
|
||||
tagBgColor = AppTheme.colors.themeUi,
|
||||
borderColor = AppTheme.colors.textSecondary,
|
||||
isLoading = false
|
||||
)
|
||||
TagView(
|
||||
modifier = Modifier.padding(start = 5.dp),
|
||||
tagText = "积分${myPoints?.coinCount ?: "10086"}",
|
||||
tagBgColor = AppTheme.colors.themeUi,
|
||||
borderColor = AppTheme.colors.textSecondary,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//我的收藏、我的分享、历史记录、积分排行榜
|
||||
@Composable
|
||||
private fun UserOptionsItem(
|
||||
onCollectClick: () -> Unit,
|
||||
onMyShareClick: () -> Unit,
|
||||
onHistoryClick: () -> Unit,
|
||||
onRankingClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(top = 20.dp)
|
||||
) {
|
||||
ProfileOptionItem(
|
||||
modifier = Modifier.weight(1f),
|
||||
title = "我的收藏",
|
||||
iconRes = Icons.Default.FavoriteBorder
|
||||
) {
|
||||
onCollectClick.invoke()
|
||||
}
|
||||
ProfileOptionItem(
|
||||
modifier = Modifier.weight(1f),
|
||||
title = "我的文章",
|
||||
iconRes = painterResource(R.drawable.ic_article)
|
||||
) {
|
||||
onMyShareClick.invoke()
|
||||
}
|
||||
ProfileOptionItem(
|
||||
modifier = Modifier.weight(1f),
|
||||
title = "历史浏览",
|
||||
iconRes = painterResource(R.drawable.ic_history_record)
|
||||
) {
|
||||
onHistoryClick.invoke()
|
||||
}
|
||||
ProfileOptionItem(
|
||||
modifier = Modifier.weight(1f),
|
||||
title = "积分排行",
|
||||
iconRes = painterResource(R.drawable.ic_ranking)
|
||||
) {
|
||||
onRankingClick.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileOptionItem(
|
||||
modifier: Modifier,
|
||||
title: String,
|
||||
iconRes: Any,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Column(modifier = modifier.clickable { onClick() }) {
|
||||
when (iconRes) {
|
||||
is Painter -> {
|
||||
Icon(
|
||||
painter = iconRes,
|
||||
contentDescription = null,
|
||||
tint = AppTheme.colors.icon,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
is ImageVector -> {
|
||||
Icon(
|
||||
imageVector = iconRes,
|
||||
contentDescription = null,
|
||||
tint = AppTheme.colors.icon,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MiniTitle(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.padding(top = 5.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.zj.wanandroid.ui.page.main.profile
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.zj.wanandroid.data.bean.UserInfo
|
||||
import com.zj.wanandroid.utils.AppUserUtil
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ProfileViewModel @Inject constructor() : ViewModel() {
|
||||
var viewStates by mutableStateOf(ProfileViewState())
|
||||
private set
|
||||
|
||||
fun dispatch(action: ProfileViewAction) {
|
||||
when (action) {
|
||||
is ProfileViewAction.OnStart -> onStart()
|
||||
is ProfileViewAction.ShowLogoutDialog -> showLogout()
|
||||
is ProfileViewAction.DismissLogoutDialog -> dismissLogout()
|
||||
is ProfileViewAction.Logout -> logout()
|
||||
}
|
||||
}
|
||||
|
||||
private fun logout() {
|
||||
AppUserUtil.onLogOut()
|
||||
viewStates = viewStates.copy(isLogged = false, showLogout = false, userInfo = null)
|
||||
}
|
||||
|
||||
private fun dismissLogout() {
|
||||
viewStates = viewStates.copy(showLogout = false)
|
||||
}
|
||||
|
||||
private fun showLogout() {
|
||||
viewStates = viewStates.copy(showLogout = true)
|
||||
}
|
||||
|
||||
private fun onStart() {
|
||||
viewStates =
|
||||
viewStates.copy(isLogged = AppUserUtil.isLogged, userInfo = AppUserUtil.userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
data class ProfileViewState(
|
||||
val isLogged: Boolean = AppUserUtil.isLogged,
|
||||
val userInfo: UserInfo? = AppUserUtil.userInfo,
|
||||
val showLogout: Boolean = false
|
||||
)
|
||||
|
||||
sealed class ProfileViewAction {
|
||||
object OnStart : ProfileViewAction()
|
||||
object ShowLogoutDialog : ProfileViewAction()
|
||||
object DismissLogoutDialog : ProfileViewAction()
|
||||
object Logout : ProfileViewAction()
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package com.zj.wanandroid.ui.page.search
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemsIndexed
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.zj.wanandroid.R
|
||||
import com.zj.wanandroid.data.bean.Hotkey
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import com.zj.wanandroid.theme.ToolBarHeight
|
||||
import com.zj.wanandroid.ui.page.common.RouteName
|
||||
import com.zj.wanandroid.ui.widgets.LabelTextButton
|
||||
import com.zj.wanandroid.ui.widgets.ListTitle
|
||||
import com.zj.wanandroid.ui.widgets.MediumTitle
|
||||
import com.zj.wanandroid.ui.widgets.MultiStateItemView
|
||||
import com.zj.wanandroid.utils.RouteUtils
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun SearchPage(
|
||||
navCtrl: NavHostController,
|
||||
scaffoldState: ScaffoldState,
|
||||
viewModel: SearchViewModel = hiltViewModel()
|
||||
) {
|
||||
val viewStates = viewModel.viewStates
|
||||
val keyboardCtrl = LocalSoftwareKeyboardController.current
|
||||
val hotkeys = viewStates.hotKeys
|
||||
val queries = viewStates.searchResult?.collectAsLazyPagingItems()
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
|
||||
SearchHead(
|
||||
keyWord = viewStates.searchContent,
|
||||
onTextChange = {
|
||||
viewModel.dispatch(SearchViewAction.SetSearchKey(it))
|
||||
},
|
||||
onSearchClick = {
|
||||
if (it.trim().isNotEmpty()) {
|
||||
viewModel.dispatch(SearchViewAction.Search)
|
||||
}
|
||||
keyboardCtrl?.hide()
|
||||
},
|
||||
navController = navCtrl
|
||||
)
|
||||
|
||||
LazyColumn {
|
||||
// part1. 搜索热词
|
||||
stickyHeader {
|
||||
ListTitle(title = "搜索热词")
|
||||
}
|
||||
item {
|
||||
if (hotkeys.isNotEmpty()) {
|
||||
Box {
|
||||
HotkeyItem(
|
||||
hotkeys = hotkeys,
|
||||
onSelected = { text ->
|
||||
viewModel.dispatch(SearchViewAction.SetSearchKey(text))
|
||||
viewModel.dispatch(SearchViewAction.Search)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// part2. 搜索列表
|
||||
stickyHeader {
|
||||
ListTitle(title = "搜索结果")
|
||||
}
|
||||
if (queries != null) {
|
||||
itemsIndexed(queries) { index, item ->
|
||||
MultiStateItemView(
|
||||
data = item!!,
|
||||
onSelected = { data ->
|
||||
RouteUtils.navTo(navCtrl, RouteName.WEB_VIEW, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索框
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun SearchHead(
|
||||
keyWord: String,
|
||||
onTextChange: (text: String) -> Unit,
|
||||
onSearchClick: (key: String) -> Unit,
|
||||
navController: NavHostController
|
||||
) {
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(ToolBarHeight)
|
||||
.background(AppTheme.colors.themeUi)
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(10.dp)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_back_white),
|
||||
null,
|
||||
Modifier
|
||||
.clickable(onClick = {
|
||||
navController.popBackStack()
|
||||
})
|
||||
.align(Alignment.CenterVertically)
|
||||
.size(20.dp)
|
||||
.padding(end = 10.dp),
|
||||
tint = AppTheme.colors.mainColor
|
||||
)
|
||||
BasicTextField(
|
||||
value = keyWord,
|
||||
onValueChange = { onTextChange(it) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.height(28.dp)
|
||||
.background(
|
||||
color = AppTheme.colors.mainColor,
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
)
|
||||
.padding(start = 10.dp, top = 4.dp)
|
||||
.align(Alignment.CenterVertically),
|
||||
maxLines = 1,
|
||||
singleLine = true,
|
||||
textStyle = TextStyle(
|
||||
color = AppTheme.colors.textSecondary,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = { onSearchClick(keyWord) }),
|
||||
)
|
||||
MediumTitle(
|
||||
title = "搜索",
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(start = 10.dp)
|
||||
.combinedClickable(onClick = { onSearchClick(keyWord) }),
|
||||
color = AppTheme.colors.mainColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索热词的item
|
||||
*/
|
||||
@Composable
|
||||
fun HotkeyItem(
|
||||
hotkeys: List<Hotkey>,
|
||||
onSelected: (key: String) -> Unit
|
||||
) {
|
||||
FlowRow(Modifier.padding(10.dp)) {
|
||||
hotkeys.forEach {
|
||||
LabelTextButton(
|
||||
text = it.name ?: "",
|
||||
isSelect = false,
|
||||
modifier = Modifier.padding(end = 5.dp, bottom = 5.dp),
|
||||
onClick = {
|
||||
onSelected(it.name!!)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.zj.wanandroid.ui.page.search
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import com.zj.wanandroid.common.paging.simplePager
|
||||
import com.zj.wanandroid.data.bean.Hotkey
|
||||
import com.zj.wanandroid.data.http.HttpService
|
||||
import com.zj.wanandroid.ui.page.main.home.square.PagingArticle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SearchViewModel @Inject constructor(
|
||||
private var service: HttpService
|
||||
) : ViewModel() {
|
||||
var viewStates by mutableStateOf(SearchViewState())
|
||||
private set
|
||||
|
||||
init {
|
||||
dispatch(SearchViewAction.GetHotKeys)
|
||||
}
|
||||
|
||||
fun dispatch(action: SearchViewAction) {
|
||||
when (action) {
|
||||
is SearchViewAction.Search -> search()
|
||||
is SearchViewAction.GetHotKeys -> getHotKeys()
|
||||
is SearchViewAction.SetSearchKey -> setSearchKey(action.key)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSearchKey(key: String) {
|
||||
viewStates = viewStates.copy(searchContent = key)
|
||||
}
|
||||
|
||||
private fun getHotKeys() {
|
||||
viewModelScope.launch {
|
||||
flow {
|
||||
emit(service.getHotkeys())
|
||||
}.map {
|
||||
it.data ?: emptyList()
|
||||
}.onEach {
|
||||
viewStates = viewStates.copy(hotKeys = it)
|
||||
}.catch {
|
||||
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun search() {
|
||||
val key = viewStates.searchContent
|
||||
val paging = simplePager {
|
||||
service.queryArticle(page = it, key = key)
|
||||
}.cachedIn(viewModelScope)
|
||||
viewStates = viewStates.copy(searchResult = paging)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class SearchViewState(
|
||||
val searchResult: PagingArticle? = null,
|
||||
val searchContent: String = "",
|
||||
val hotKeys: List<Hotkey> = emptyList()
|
||||
)
|
||||
|
||||
sealed class SearchViewAction {
|
||||
object GetHotKeys : SearchViewAction()
|
||||
object Search : SearchViewAction()
|
||||
data class SetSearchKey(val key: String) : SearchViewAction()
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.zj.wanandroid.ui.page.splash
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import com.zj.wanandroid.theme.white
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun SplashPage(onNextPage: () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppTheme.colors.themeUi), contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
delay(500)
|
||||
onNextPage.invoke()
|
||||
}
|
||||
Text(
|
||||
text = "Wan Android",
|
||||
fontSize = 32.sp,
|
||||
color = white,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(0.dp, 150.dp, 0.dp, 0.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.zj.wanandroid.ui.page.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebView
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.navigation.NavHostController
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import com.zj.wanandroid.data.bean.WebData
|
||||
import com.zj.wanandroid.theme.ToolBarHeight
|
||||
import com.zj.wanandroid.utils.SizeUtils
|
||||
import com.zj.wanandroid.R
|
||||
import com.zj.wanandroid.ui.widgets.AppToolsBar
|
||||
import com.zj.wanandroid.utils.RouteUtils.back
|
||||
|
||||
@SuppressLint("UseCompatLoadingForDrawables")
|
||||
@Composable
|
||||
fun WebViewPage(
|
||||
webData: WebData,
|
||||
navCtrl: NavHostController
|
||||
) {
|
||||
var ctrl: WebViewCtrl? by remember { mutableStateOf(null) }
|
||||
Box {
|
||||
var isRefreshing: Boolean by remember { mutableStateOf(false) }
|
||||
val refreshState = rememberSwipeRefreshState(isRefreshing)
|
||||
AndroidView(
|
||||
modifier = Modifier
|
||||
.padding(top = ToolBarHeight)
|
||||
.fillMaxSize(),
|
||||
factory = { context ->
|
||||
FrameLayout(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
val progressView = ProgressBar(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
SizeUtils.dp2px(2f)
|
||||
)
|
||||
progressDrawable =
|
||||
context.resources.getDrawable(R.drawable.horizontal_progressbar)
|
||||
indeterminateTintList =
|
||||
ColorStateList.valueOf(context.resources.getColor(R.color.teal_200))
|
||||
}
|
||||
val webView = WebView(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
addView(webView)
|
||||
addView(progressView)
|
||||
ctrl = WebViewCtrl(this, webData.url, onWebCall = { isFinish ->
|
||||
isRefreshing = !isFinish
|
||||
})
|
||||
ctrl?.initSettings()
|
||||
}
|
||||
|
||||
},
|
||||
update = {
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
AppToolsBar(title = webData.title ?: "标题", onBack = {
|
||||
ctrl?.onDestroy()
|
||||
navCtrl.back()
|
||||
})
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package com.zj.wanandroid.ui.page.webview
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.http.SslError
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.webkit.*
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
class WebViewCtrl(
|
||||
private val mView: FrameLayout,
|
||||
private var linkUrl: String,
|
||||
private val onWebCall: (isFinish: Boolean) -> Unit
|
||||
) {
|
||||
|
||||
private val webView by lazy { mView.getChildAt(0) as WebView }
|
||||
private val progressBar by lazy { mView.getChildAt(1) as ProgressBar }
|
||||
|
||||
fun initSettings() {
|
||||
onWebCall(false)
|
||||
setWebSettings()
|
||||
setupWebClient()
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
mView.removeAllViews()
|
||||
webView.destroy()
|
||||
}
|
||||
|
||||
private fun setWebSettings() {
|
||||
val webSettings = webView.settings
|
||||
//如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
|
||||
webSettings.javaScriptEnabled = false
|
||||
//设置自适应屏幕,两者合用
|
||||
webSettings.useWideViewPort = true //将图片调整到适合webview的大小
|
||||
webSettings.loadWithOverviewMode = true // 缩放至屏幕的大小
|
||||
//缩放操作
|
||||
webSettings.setSupportZoom(true) //支持缩放,默认为true。是下面那个的前提。
|
||||
webSettings.builtInZoomControls = true //设置内置的缩放控件。若为false,则该WebView不可缩放
|
||||
webSettings.displayZoomControls = false //隐藏原生的缩放控件
|
||||
|
||||
//其他细节操作
|
||||
webSettings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK //关闭webview中缓存
|
||||
webSettings.allowFileAccess = true //设置可以访问文件
|
||||
webSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口
|
||||
webSettings.loadsImagesAutomatically = true //支持自动加载图片
|
||||
webSettings.defaultTextEncodingName = "UTF-8"//设置编码格式
|
||||
}
|
||||
|
||||
|
||||
private fun setupWebClient() {
|
||||
webView.webViewClient = NewWebViewClient()
|
||||
webView.webChromeClient = ProgressWebViewChromeClient()
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
webView.loadUrl(linkUrl)
|
||||
}
|
||||
|
||||
|
||||
inner class ProgressWebViewChromeClient : WebChromeClient() {
|
||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||
super.onProgressChanged(view, newProgress)
|
||||
progressBar.progress = newProgress
|
||||
}
|
||||
|
||||
override fun onReceivedTitle(view: WebView?, title: String?) {
|
||||
super.onReceivedTitle(view, title)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inner class NewWebViewClient : WebViewClient() {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): Boolean {
|
||||
linkUrl = request?.url.toString()
|
||||
return super.shouldOverrideUrlLoading(view, request)
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
|
||||
linkUrl = url?:"NullUrlString"
|
||||
return super.shouldOverrideUrlLoading(view, url)
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
super.onPageStarted(view, url, favicon)
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
progressBar.visibility = View.GONE
|
||||
onWebCall(true)
|
||||
super.onPageFinished(view, url)
|
||||
}
|
||||
|
||||
override fun onReceivedSslError(
|
||||
view: WebView?,
|
||||
handler: SslErrorHandler?,
|
||||
error: SslError?
|
||||
) {
|
||||
handler?.proceed()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
package com.zj.wanandroid.ui.widgets
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
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 com.zj.wanandroid.R
|
||||
import com.zj.wanandroid.theme.AppShapes
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* 轮播图
|
||||
* [timeMillis] 停留时间
|
||||
* [loadImage] 加载中显示的布局
|
||||
* [indicatorAlignment] 指示点的的位置,默认是轮播图下方的中间,带一点padding
|
||||
* [onClick] 轮播图点击事件
|
||||
*/
|
||||
@ExperimentalPagerApi
|
||||
@Composable
|
||||
fun Banner(
|
||||
list: List<BannerData>?,
|
||||
timeMillis: Long = 3000,
|
||||
@DrawableRes loadImage: Int = R.drawable.no_banner,
|
||||
indicatorAlignment: Alignment = Alignment.BottomCenter,
|
||||
onClick: (link: String, title: String) -> Unit
|
||||
) {
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.background)
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.height(120.dp)
|
||||
) {
|
||||
|
||||
if (list == null) {
|
||||
//加载中的图片
|
||||
Image(
|
||||
painterResource(loadImage),
|
||||
modifier = Modifier.fillMaxSize().clip(shape = RoundedCornerShape(16.dp)),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
val pagerState = rememberPagerState(
|
||||
//初始页面
|
||||
initialPage = 0
|
||||
)
|
||||
|
||||
//监听动画执行
|
||||
var executeChangePage by remember { mutableStateOf(false) }
|
||||
var currentPageIndex = 0
|
||||
|
||||
//自动滚动
|
||||
LaunchedEffect(pagerState.currentPage, executeChangePage) {
|
||||
if (pagerState.pageCount > 0) {
|
||||
delay(timeMillis)
|
||||
//这里直接+1就可以循环,前提是infiniteLoop == true
|
||||
pagerState.animateScrollToPage((pagerState.currentPage + 1) % (pagerState.pageCount))
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
count = list.size,
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.pointerInput(pagerState.currentPage) {
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
//PointerEventPass.Initial - 本控件优先处理手势,处理后再交给子组件
|
||||
val event = awaitPointerEvent(PointerEventPass.Initial)
|
||||
//获取到第一根按下的手指
|
||||
val dragEvent = event.changes.firstOrNull()
|
||||
when {
|
||||
//当前移动手势是否已被消费
|
||||
dragEvent!!.positionChangeConsumed() -> {
|
||||
return@awaitPointerEventScope
|
||||
}
|
||||
//是否已经按下(忽略按下手势已消费标记)
|
||||
dragEvent.changedToDownIgnoreConsumed() -> {
|
||||
//记录下当前的页面索引值
|
||||
currentPageIndex = pagerState.currentPage
|
||||
}
|
||||
//是否已经抬起(忽略按下手势已消费标记)
|
||||
dragEvent.changedToUpIgnoreConsumed() -> {
|
||||
//当页面没有任何滚动/动画的时候pagerState.targetPage为null,这个时候是单击事件
|
||||
if (pagerState.targetPage == null) return@awaitPointerEventScope
|
||||
//当pageCount大于1,且手指抬起时如果页面没有改变,就手动触发动画
|
||||
if (currentPageIndex == pagerState.currentPage && pagerState.pageCount > 1) {
|
||||
executeChangePage = !executeChangePage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.clickable {
|
||||
with(list[pagerState.currentPage]) {
|
||||
onClick.invoke(linkUrl, title)
|
||||
}
|
||||
}
|
||||
.fillMaxSize(),
|
||||
) { page ->
|
||||
Image(
|
||||
painter = rememberCoilPainter(list[page].imageUrl),
|
||||
modifier = Modifier.fillMaxSize().clip(shape = RoundedCornerShape(16.dp)),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(indicatorAlignment)
|
||||
.padding(bottom = 6.dp, start = 6.dp, end = 6.dp)
|
||||
) {
|
||||
|
||||
//指示点
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
for (i in list.indices) {
|
||||
//大小
|
||||
var size by remember { mutableStateOf(5.dp) }
|
||||
size = if (pagerState.currentPage == i) 7.dp else 5.dp
|
||||
|
||||
//颜色
|
||||
val color =
|
||||
if (pagerState.currentPage == i) MaterialTheme.colors.primary else Color.Gray
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
//当size改变的时候以动画的形式改变
|
||||
.animateContentSize()
|
||||
.size(size)
|
||||
)
|
||||
//指示点间的间隔
|
||||
if (i != list.lastIndex) Spacer(
|
||||
modifier = Modifier
|
||||
.height(0.dp)
|
||||
.width(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮播图数据
|
||||
*/
|
||||
data class BannerData(
|
||||
val title: String,
|
||||
val imageUrl: String,
|
||||
val linkUrl: String
|
||||
)
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package com.zj.wanandroid.ui.widgets
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.accompanist.placeholder.material.placeholder
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import com.zj.wanandroid.theme.buttonCorner
|
||||
import com.zj.wanandroid.theme.buttonHeight
|
||||
import com.zj.wanandroid.theme.white1
|
||||
import org.jetbrains.annotations.NotNull
|
||||
|
||||
|
||||
@Composable
|
||||
fun AppButton(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
bgColor: Color = AppTheme.colors.secondBtnBg,
|
||||
textColor: Color = AppTheme.colors.textPrimary,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(buttonHeight)
|
||||
.background(color = bgColor, shape = RoundedCornerShape(buttonCorner))
|
||||
.clickable {
|
||||
onClick()
|
||||
}
|
||||
) {
|
||||
TextContent(text = text, color = textColor, modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun PrimaryButton(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
AppButton(
|
||||
text = text,
|
||||
modifier = modifier,
|
||||
textColor = AppTheme.colors.textPrimary,
|
||||
onClick = onClick,
|
||||
bgColor = AppTheme.colors.themeUi
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SecondlyButton(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
AppButton(
|
||||
text = text,
|
||||
modifier = modifier,
|
||||
textColor = AppTheme.colors.textSecondary,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LabelTextButton(
|
||||
@NotNull text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
isSelect: Boolean = true,
|
||||
specTextColor: Color? = null,
|
||||
cornerValue: Dp = 25.dp / 2,
|
||||
isLoading: Boolean = false,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier
|
||||
.height(25.dp)
|
||||
.clip(shape = RoundedCornerShape(cornerValue))
|
||||
.background(
|
||||
color = if (isSelect && !isLoading) AppTheme.colors.themeUi else AppTheme.colors.secondBtnBg,
|
||||
)
|
||||
.padding(
|
||||
horizontal = 10.dp,
|
||||
vertical = 3.dp
|
||||
)
|
||||
.combinedClickable(
|
||||
enabled = !isLoading,
|
||||
onClick = { onClick?.invoke() },
|
||||
onLongClick = { onLongClick?.invoke() }
|
||||
)
|
||||
.placeholder(
|
||||
visible = isLoading,
|
||||
color = AppTheme.colors.placeholder
|
||||
),
|
||||
fontSize = 13.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
color = specTextColor ?: if (isSelect) white1 else AppTheme.colors.textSecondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
package com.zj.wanandroid.ui.widgets
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import com.google.accompanist.placeholder.material.placeholder
|
||||
import com.zj.wanandroid.theme.*
|
||||
import com.zj.wanandroid.ui.page.common.BottomNavRoute
|
||||
|
||||
/**
|
||||
* 普通标题栏头部
|
||||
*/
|
||||
@Composable
|
||||
fun AppToolsBar(
|
||||
title: String,
|
||||
rightText: String? = null,
|
||||
onBack: (() -> Unit)? = null,
|
||||
onRightClick: (() -> Unit)? = null,
|
||||
imageVector: ImageVector? = null,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(ToolBarHeight)
|
||||
.background(AppTheme.colors.themeUi)
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
if (onBack != null) {
|
||||
Icon(
|
||||
Icons.Default.ArrowBack,
|
||||
null,
|
||||
Modifier
|
||||
.clickable(onClick = onBack)
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(12.dp),
|
||||
tint = AppTheme.colors.mainColor
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (!rightText.isNullOrEmpty() && imageVector == null) {
|
||||
TextContent(
|
||||
text = rightText,
|
||||
color = AppTheme.colors.mainColor,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(horizontal = 20.dp)
|
||||
.clickable { onRightClick?.invoke() }
|
||||
)
|
||||
}
|
||||
|
||||
if (imageVector != null) {
|
||||
Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
tint = AppTheme.colors.mainColor,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(end = 12.dp)
|
||||
.clickable {
|
||||
onRightClick?.invoke()
|
||||
})
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(horizontal = 40.dp),
|
||||
color = AppTheme.colors.mainColor,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = if (title.length > 14) H5 else ToolBarTitleSize,
|
||||
fontWeight = FontWeight.W500,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun HomeSearchBar(
|
||||
onSearchClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(SearchBarHeight)
|
||||
.background(color = AppTheme.colors.themeUi)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 32.dp)
|
||||
.height(30.dp)
|
||||
.align(alignment = Alignment.Top)
|
||||
.weight(1f)
|
||||
.background(
|
||||
color = AppTheme.colors.mainColor,
|
||||
shape = RoundedCornerShape(12.5.dp)
|
||||
)
|
||||
.clickable { onSearchClick() }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = com.zj.wanandroid.R.drawable.ic_search),
|
||||
contentDescription = "搜索",
|
||||
tint = AppTheme.colors.themeUi,
|
||||
modifier = Modifier
|
||||
.size(25.dp)
|
||||
.padding(start = 10.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(start = 10.dp)
|
||||
) {
|
||||
Text(text = "搜索关键词以空格形式隔开", fontSize = 13.sp, color = AppTheme.colors.textSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* TabLayout
|
||||
*/
|
||||
@Composable
|
||||
fun TextTabBar(
|
||||
index: Int,
|
||||
tabTexts: List<TabTitle>,
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlign: Alignment = Alignment.Center,
|
||||
bgColor: Color = AppTheme.colors.themeUi,
|
||||
contentColor: Color = Color.White,
|
||||
onTabSelected: ((index: Int) -> Unit)? = null
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(54.dp)
|
||||
.background(bgColor)
|
||||
.horizontalScroll(state = rememberScrollState())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(contentAlign)
|
||||
) {
|
||||
tabTexts.forEachIndexed { i, tabTitle ->
|
||||
Text(
|
||||
text = tabTitle.text,
|
||||
fontSize = if (index == i) 20.sp else 15.sp,
|
||||
fontWeight = if (index == i) FontWeight.SemiBold else FontWeight.Normal,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(horizontal = 10.dp)
|
||||
.clickable {
|
||||
onTabSelected?.invoke(i)
|
||||
},
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class TabTitle(
|
||||
val id: Int,
|
||||
val text: String,
|
||||
var cachePosition: Int = 0,
|
||||
var selected: Boolean = false
|
||||
)
|
||||
|
||||
|
||||
@Composable
|
||||
fun TabBar(
|
||||
index: Int = 0,
|
||||
tabTexts: List<String>,
|
||||
modifier: Modifier = Modifier,
|
||||
bgColor: Color,
|
||||
contentColor: Color,
|
||||
onTabSelected: ((index: Int) -> Unit)?,
|
||||
isScrollable: Boolean = true
|
||||
) {
|
||||
if (isScrollable) {
|
||||
ScrollableTabRow(
|
||||
selectedTabIndex = index,
|
||||
modifier = modifier.height(TabBarHeight),
|
||||
edgePadding = 0.dp,
|
||||
backgroundColor = bgColor,
|
||||
contentColor = contentColor,
|
||||
) {
|
||||
//var offset: Float by remember { mutableStateOf(0f) }
|
||||
tabTexts.forEachIndexed { i, tabText ->
|
||||
Text(
|
||||
text = tabText,
|
||||
fontSize = if (index == i) 20.sp else 15.sp,
|
||||
fontWeight = if (index == i) FontWeight.SemiBold else FontWeight.Normal,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.clickable {
|
||||
if (onTabSelected != null) {
|
||||
onTabSelected(i)
|
||||
}
|
||||
},
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TabRow(
|
||||
selectedTabIndex = index,
|
||||
modifier = modifier.height(TabBarHeight),
|
||||
backgroundColor = bgColor,
|
||||
contentColor = contentColor,
|
||||
) {
|
||||
tabTexts.forEachIndexed { i, tabText ->
|
||||
Text(
|
||||
text = tabText,
|
||||
fontSize = if (index == i) 20.sp else 15.sp,
|
||||
fontWeight = if (index == i) FontWeight.SemiBold else FontWeight.Normal,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.clickable {
|
||||
if (onTabSelected != null) {
|
||||
onTabSelected(i)
|
||||
}
|
||||
},
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomNavBarView(navCtrl: NavHostController) {
|
||||
val bottomNavList = listOf(
|
||||
BottomNavRoute.Home,
|
||||
BottomNavRoute.Category,
|
||||
BottomNavRoute.Collection,
|
||||
BottomNavRoute.Profile
|
||||
)
|
||||
BottomNavigation {
|
||||
val navBackStackEntry by navCtrl.currentBackStackEntryAsState()
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
bottomNavList.forEach { screen ->
|
||||
BottomNavigationItem(
|
||||
modifier = Modifier.background(AppTheme.colors.themeUi),
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = screen.icon,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(text = stringResource(screen.stringId)) },
|
||||
selected = currentDestination?.hierarchy?.any { it.route == screen.routeName } == true,
|
||||
onClick = {
|
||||
println("BottomNavView当前路由 ===> ${currentDestination?.hierarchy?.toList()}")
|
||||
println("当前路由栈 ===> ${navCtrl.graph.nodes}")
|
||||
if (currentDestination?.route != screen.routeName) {
|
||||
navCtrl.navigate(screen.routeName) {
|
||||
popUpTo(navCtrl.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun EmptyView(
|
||||
tips: String = "啥都没有~",
|
||||
imageVector: ImageVector = Icons.Default.Info,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(1f)
|
||||
.defaultMinSize(minHeight = 480.dp)
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.wrapContentSize()
|
||||
.align(Alignment.Center)
|
||||
.clickable { onClick.invoke() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
tint = AppTheme.colors.textSecondary,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
TextContent(text = tips, modifier = Modifier.padding(top = 10.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwitchTabBar(
|
||||
titles: MutableList<TabTitle>,
|
||||
heightValue: Dp? = null,
|
||||
selectIndex: Int,
|
||||
onSwitchClick: (index: Int) -> Unit
|
||||
) {
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(heightValue ?: ToolBarHeight)
|
||||
.background(color = AppTheme.colors.themeUi)
|
||||
.padding(vertical = 5.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
) {
|
||||
titles.forEachIndexed { index, tabTitle ->
|
||||
MediumTitle(
|
||||
title = tabTitle.text,
|
||||
color = if (index == selectIndex) AppTheme.colors.textPrimary
|
||||
else AppTheme.colors.textSecondary,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(100.dp, 24.dp)
|
||||
.padding(horizontal = 10.dp)
|
||||
.clickable { onSwitchClick.invoke(index) },
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(1.dp)
|
||||
.height(16.dp)
|
||||
.background(color = AppTheme.colors.textSecondary)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun TagView(
|
||||
modifier: Modifier = Modifier,
|
||||
tagText: String,
|
||||
tagBgColor: Color = AppTheme.colors.background,
|
||||
borderColor: Color = AppTheme.colors.themeUi,
|
||||
tagTextColor: Color = AppTheme.colors.textSecondary,
|
||||
isLoading: Boolean = false,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.wrapContentSize()
|
||||
.background(color = tagBgColor)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.border(width = 1.dp, color = borderColor)
|
||||
.placeholder(
|
||||
visible = isLoading,
|
||||
color = AppTheme.colors.placeholder
|
||||
)
|
||||
) {
|
||||
MiniTitle(
|
||||
text = tagText,
|
||||
color = tagTextColor,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(horizontal = 5.dp, vertical = 0.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package com.zj.wanandroid.ui.widgets
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
|
||||
|
||||
@Composable
|
||||
fun SampleAlertDialog(
|
||||
title: String,
|
||||
content: String,
|
||||
cancelText: String = "取消",
|
||||
confirmText: String = "继续",
|
||||
onConfirmClick: () -> Unit,
|
||||
//onCancelClick: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
MediumTitle(title = title)
|
||||
},
|
||||
text = {
|
||||
TextContent(text = content)
|
||||
},
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirmClick.invoke()
|
||||
onDismiss.invoke()
|
||||
}) {
|
||||
TextContent(text = confirmText, color = AppTheme.colors.textPrimary)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { onDismiss.invoke() }) {
|
||||
TextContent(text = cancelText)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 30.dp)
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectAlertDialog(
|
||||
title: String,
|
||||
content: String,
|
||||
primaryButtonText: String,
|
||||
secondButtonText: String,
|
||||
onPrimaryButtonClick: () -> Unit,
|
||||
onSecondButtonClick: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card {
|
||||
Column(Modifier.padding(20.dp)) {
|
||||
MediumTitle(title = title, modifier = Modifier.padding(bottom = 20.dp))
|
||||
TextContent(text = content, modifier = Modifier.padding(bottom = 20.dp))
|
||||
PrimaryButton(text = primaryButtonText, Modifier.padding(bottom = 10.dp)) {
|
||||
onPrimaryButtonClick.invoke()
|
||||
onDismiss.invoke()
|
||||
}
|
||||
SecondlyButton(text = secondButtonText) {
|
||||
onSecondButtonClick.invoke()
|
||||
onDismiss.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoDialog(
|
||||
title: String = "关于我",
|
||||
vararg content: String,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(dismissOnClickOutside = true),
|
||||
title = {
|
||||
MediumTitle(title = title)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
Modifier.defaultMinSize(minWidth = 300.dp)
|
||||
) {
|
||||
content.forEach {
|
||||
TextContent(
|
||||
text = it,
|
||||
modifier = Modifier.padding(bottom = 10.dp),
|
||||
canCopy = true
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextContent(
|
||||
text = "关闭",
|
||||
modifier = Modifier
|
||||
.padding(end = 18.dp, bottom = 18.dp)
|
||||
.clickable { onDismiss.invoke() }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package com.zj.wanandroid.ui.widgets
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.compose.ui.platform.LocalTextInputService
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextInputSession
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LoginEditView(
|
||||
text: String,
|
||||
labelText: String,
|
||||
hintText: String?,
|
||||
onValueChanged: (String) -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isPassword: Boolean = false
|
||||
) {
|
||||
BasicLabelEditView(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
labelText = labelText,
|
||||
labelTextColor = AppTheme.colors.mainColor,
|
||||
hintText = hintText ?: "",
|
||||
onValueChanged = { onValueChanged.invoke(it) },
|
||||
deleteIconColor = AppTheme.colors.mainColor,
|
||||
onDeleteClick = { onDeleteClick.invoke() },
|
||||
inputCursorColor = AppTheme.colors.mainColor,
|
||||
inputTextColor = AppTheme.colors.mainColor,
|
||||
borderColor = AppTheme.colors.mainColor,
|
||||
keyboardType = if (isPassword) KeyboardType.Password else KeyboardType.Text
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LabelEditView(
|
||||
text: String,
|
||||
labelText: String,
|
||||
hintText: String?,
|
||||
onValueChanged: (String) -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BasicLabelEditView(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
labelText = labelText,
|
||||
hintText = hintText ?: "",
|
||||
onValueChanged = { onValueChanged.invoke(it) },
|
||||
onDeleteClick = { onDeleteClick.invoke() }
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun BasicLabelEditView(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
labelText: String,
|
||||
labelTextColor: Color = AppTheme.colors.textPrimary,
|
||||
hintText: String = "",
|
||||
deleteIconColor: Color = AppTheme.colors.themeUi,
|
||||
onValueChanged: (String) -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
inputCursorColor: Color = AppTheme.colors.themeUi,
|
||||
inputTextColor: Color = AppTheme.colors.textPrimary,
|
||||
borderColor: Color = AppTheme.colors.textSecondary,
|
||||
keyboardType: KeyboardType = KeyboardType.Text,
|
||||
isHideKeyboard: Boolean = true,
|
||||
) {
|
||||
val keyboardService = LocalTextInputService.current
|
||||
|
||||
TextField(
|
||||
value = text,
|
||||
onValueChange = { onValueChanged(it) },
|
||||
textStyle = TextStyle(lineHeight = 24.sp, fontSize = 16.sp),
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 20.dp)
|
||||
.pointerInteropFilter { false },
|
||||
label = {
|
||||
MediumTitle(
|
||||
labelText,
|
||||
color = labelTextColor,
|
||||
modifier = Modifier.padding(bottom = 10.dp)
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
TextContent(hintText)
|
||||
},
|
||||
trailingIcon = {
|
||||
if (text.isNotEmpty()) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Clear,
|
||||
contentDescription = null,
|
||||
tint = deleteIconColor,
|
||||
modifier = Modifier.clickable { onDeleteClick() }
|
||||
)
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
maxLines = 1,
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = borderColor,
|
||||
unfocusedBorderColor = borderColor,
|
||||
textColor = inputTextColor,
|
||||
placeholderColor = AppTheme.colors.textSecondary,
|
||||
cursorColor = inputCursorColor
|
||||
),
|
||||
keyboardActions = if (!isHideKeyboard) KeyboardActions.Default
|
||||
else KeyboardActions { keyboardService?.hideSoftwareKeyboard() },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = keyboardType,
|
||||
imeAction = ImeAction.Next
|
||||
)
|
||||
)
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.zj.wanandroid.ui.widgets
|
||||
|
||||
import android.R
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
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
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zj.wanandroid.data.http.PageState
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
|
||||
/**
|
||||
* 通过State进行控制的Loading、Content、Error页面
|
||||
*
|
||||
* @param pageState 数据State
|
||||
* @param onRetry 错误时的点击事件
|
||||
* @param content 数据加载成功时应显示的可组合项
|
||||
*/
|
||||
@Composable
|
||||
fun LcePage(
|
||||
pageState: PageState,
|
||||
onRetry: () -> Unit,
|
||||
content: @Composable () -> Unit
|
||||
) = when (pageState) {
|
||||
is PageState.Loading -> PageLoading()
|
||||
is PageState.Error -> ErrorContent(onRetry)
|
||||
is PageState.Success -> {
|
||||
if (pageState.isEmpty) {
|
||||
PageEmpty()
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PageLoading() {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||
CircularProgressIndicator(
|
||||
color = AppTheme.colors.themeUi,
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.width(50.dp)
|
||||
.height(50.dp)
|
||||
)
|
||||
Text(
|
||||
text = "正在加载中",
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(top = 10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PageEmpty() {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.stat_notify_error),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(Color.Red),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Text(
|
||||
text = "暂无相关内容",
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(top = 10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
package com.zj.wanandroid.ui.widgets
|
||||
|
||||
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.Card
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import com.google.accompanist.placeholder.material.placeholder
|
||||
import com.zj.wanandroid.data.bean.Article
|
||||
import com.zj.wanandroid.data.bean.CollectBean
|
||||
import com.zj.wanandroid.data.bean.WebData
|
||||
import com.zj.wanandroid.theme.*
|
||||
import com.zj.wanandroid.utils.RegexUtils
|
||||
|
||||
@Composable
|
||||
fun ListTitle(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
subTitle: String = "",
|
||||
isLoading: Boolean = false,
|
||||
onSubtitleClick: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.placeholder(false)
|
||||
.fillMaxWidth()
|
||||
.height(ListTitleHeight)
|
||||
.background(color = AppTheme.colors.background)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.width(5.dp)
|
||||
.height(16.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
.background(color = AppTheme.colors.textPrimary)
|
||||
)
|
||||
MediumTitle(
|
||||
title = title,
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
isLoading = isLoading
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
TextContent(
|
||||
text = subTitle,
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp)
|
||||
.clickable {
|
||||
onSubtitleClick.invoke()
|
||||
},
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun CollectListItemView(
|
||||
collect: CollectBean,
|
||||
isLoading: Boolean = false,
|
||||
onClick: () -> Unit = {},
|
||||
onDeleteClick: () -> Unit = {}
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp, vertical = 5.dp)
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth()
|
||||
.background(color = AppTheme.colors.card)
|
||||
.clickable(enabled = !isLoading) { onClick.invoke() }
|
||||
) {
|
||||
ConstraintLayout(
|
||||
modifier = Modifier.padding(20.dp)
|
||||
) {
|
||||
val (name, publishIcon, publishTime, title, delete) = createRefs()
|
||||
MediumTitle(
|
||||
title = if (collect.author.isNotEmpty()) collect.author else "无名",
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minWidth = 100.dp)
|
||||
.constrainAs(name) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(parent.start)
|
||||
},
|
||||
isLoading = isLoading
|
||||
)
|
||||
MiniTitle(
|
||||
text = RegexUtils().timestamp(collect.niceDate) ?: "2021",
|
||||
modifier = Modifier
|
||||
.constrainAs(publishTime) {
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(parent.end)
|
||||
bottom.linkTo(publishIcon.bottom)
|
||||
}
|
||||
.defaultMinSize(minWidth = 40.dp),
|
||||
isLoading = isLoading
|
||||
)
|
||||
TimerIcon(
|
||||
modifier = Modifier
|
||||
.constrainAs(publishIcon) {
|
||||
top.linkTo(parent.top, margin = 2.5.dp)
|
||||
end.linkTo(publishTime.start)
|
||||
},
|
||||
isLoading = isLoading
|
||||
)
|
||||
TextContent(
|
||||
text = collect.title,
|
||||
maxLines = 3,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(top = 10.dp, bottom = 20.dp)
|
||||
.constrainAs(title) {
|
||||
top.linkTo(name.bottom)
|
||||
end.linkTo(parent.end)
|
||||
},
|
||||
isLoading = isLoading
|
||||
)
|
||||
if (!isLoading) {
|
||||
DeleteIcon(
|
||||
modifier = Modifier
|
||||
.padding(top = 5.dp)
|
||||
.constrainAs(delete) {
|
||||
top.linkTo(title.bottom)
|
||||
end.linkTo(parent.end)
|
||||
bottom.linkTo(parent.bottom)
|
||||
},
|
||||
onClick = onDeleteClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleListItemView(
|
||||
data: Article,
|
||||
isLoading: Boolean = false,
|
||||
onClick: () -> Unit = {},
|
||||
onCollectClick: (articleId: Int) -> Unit = {},
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp, vertical = 5.dp)
|
||||
.background(color = AppTheme.colors.card)
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth()
|
||||
.clickable(enabled = !isLoading) {
|
||||
onClick.invoke()
|
||||
}
|
||||
) {
|
||||
ConstraintLayout(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
) {
|
||||
val (name, publishIcon, publishTime, title, favourite) = createRefs()
|
||||
MediumTitle(
|
||||
title = data.author ?: data.shareUser ?: "作者",
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minWidth = 100.dp)
|
||||
.constrainAs(name) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(parent.start)
|
||||
},
|
||||
isLoading = isLoading
|
||||
)
|
||||
MiniTitle(
|
||||
text = RegexUtils().timestamp(data.niceDate) ?: "",
|
||||
modifier = Modifier.constrainAs(publishTime) {
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(parent.end)
|
||||
bottom.linkTo(publishIcon.bottom)
|
||||
},
|
||||
isLoading = isLoading
|
||||
)
|
||||
TimerIcon(
|
||||
modifier = Modifier
|
||||
.constrainAs(publishIcon) {
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(publishTime.start)
|
||||
},
|
||||
isLoading = isLoading
|
||||
)
|
||||
TextContent(
|
||||
text = data.title ?: "",
|
||||
maxLines = 3,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(top = 10.dp, bottom = 20.dp)
|
||||
.constrainAs(title) {
|
||||
top.linkTo(name.bottom)
|
||||
end.linkTo(parent.end)
|
||||
},
|
||||
isLoading = isLoading
|
||||
)
|
||||
FavouriteIcon(
|
||||
modifier = Modifier.constrainAs(favourite) {
|
||||
top.linkTo(title.bottom)
|
||||
end.linkTo(parent.end)
|
||||
bottom.linkTo(parent.bottom)
|
||||
},
|
||||
isFavourite = data.collect,
|
||||
onClick = {
|
||||
onCollectClick.invoke(data.id)
|
||||
},
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun MultiStateItemView(
|
||||
modifier: Modifier = Modifier,
|
||||
data: Article,
|
||||
isTop: Boolean = false,
|
||||
onSelected: (data: WebData) -> Unit = {},
|
||||
onCollectClick: (articleId: Int) -> Unit = {},
|
||||
onUserClick: (userId: Int) -> Unit = {},
|
||||
isLoading: Boolean = false,
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.padding(vertical = 5.dp, horizontal = 10.dp)
|
||||
.background(color = AppTheme.colors.card)
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = !isLoading) {
|
||||
onSelected.invoke(WebData(data.title!!, data.link!!))
|
||||
},
|
||||
shape = AppShapes.medium,
|
||||
backgroundColor = AppTheme.colors.listItem,
|
||||
) {
|
||||
ConstraintLayout(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
) {
|
||||
val (circleText, name, publishIcon, publishTime, title, chip1, chip2, tag, favourite) = createRefs()
|
||||
Text(
|
||||
text = getFirstCharFromName(data),
|
||||
modifier = Modifier
|
||||
.width(20.dp)
|
||||
.height(20.dp)
|
||||
.clip(RoundedCornerShape(20.dp / 2))
|
||||
.background(color = AppTheme.colors.themeUi)
|
||||
.constrainAs(circleText) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(parent.start)
|
||||
}
|
||||
.placeholder(
|
||||
visible = isLoading,
|
||||
color = AppTheme.colors.placeholder
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = H6,
|
||||
color = white1,
|
||||
)
|
||||
val titleModifier =
|
||||
if (isLoading) Modifier.width(80.dp) else Modifier.wrapContentWidth()
|
||||
MediumTitle(
|
||||
title = getAuthorName(data),
|
||||
modifier = titleModifier
|
||||
.constrainAs(name) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(circleText.end)
|
||||
}
|
||||
.padding(start = 5.dp)
|
||||
.clickable {
|
||||
onUserClick.invoke(data.userId)
|
||||
}
|
||||
.pointerInteropFilter { false },
|
||||
isLoading = isLoading
|
||||
)
|
||||
val dateModifier =
|
||||
if (isLoading) Modifier.width(80.dp) else Modifier.wrapContentWidth()
|
||||
MiniTitle(
|
||||
text = RegexUtils().timestamp(data.niceDate!!) ?: "1970-1-1",
|
||||
modifier = dateModifier
|
||||
.constrainAs(publishTime) {
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(parent.end)
|
||||
},
|
||||
isLoading = isLoading
|
||||
)
|
||||
TimerIcon(
|
||||
modifier = Modifier
|
||||
.padding(end = if (isLoading) 5.dp else 0.dp)
|
||||
.constrainAs(publishIcon) {
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(publishTime.start)
|
||||
bottom.linkTo(publishTime.bottom)
|
||||
},
|
||||
isLoading = isLoading
|
||||
)
|
||||
TextContent(
|
||||
text = data.title ?: "",
|
||||
maxLines = 3,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(top = 10.dp, bottom = 20.dp)
|
||||
.constrainAs(title) {
|
||||
top.linkTo(circleText.bottom)
|
||||
end.linkTo(parent.end)
|
||||
},
|
||||
isLoading = isLoading,
|
||||
)
|
||||
LabelTextButton(
|
||||
text = data.superChapterName ?: "热门",
|
||||
modifier = Modifier
|
||||
.constrainAs(chip1) {
|
||||
top.linkTo(title.bottom)
|
||||
start.linkTo(parent.start)
|
||||
bottom.linkTo(parent.bottom)
|
||||
},
|
||||
isLoading = isLoading
|
||||
)
|
||||
LabelTextButton(
|
||||
text = data.chapterName ?: "android",
|
||||
modifier = Modifier
|
||||
.constrainAs(chip2) {
|
||||
top.linkTo(title.bottom)
|
||||
start.linkTo(chip1.end, margin = 5.dp)
|
||||
bottom.linkTo(parent.bottom)
|
||||
},
|
||||
isLoading = isLoading,
|
||||
)
|
||||
FavouriteIcon(
|
||||
modifier = Modifier.constrainAs(favourite) {
|
||||
top.linkTo(title.bottom)
|
||||
end.linkTo(parent.end)
|
||||
bottom.linkTo(parent.bottom)
|
||||
},
|
||||
isFavourite = data.collect,
|
||||
onClick = {
|
||||
onCollectClick.invoke(data.id)
|
||||
},
|
||||
isLoading = isLoading
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 20.dp)
|
||||
.constrainAs(tag) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(name.end, margin = 5.dp)
|
||||
}
|
||||
) {
|
||||
if (isTop) {
|
||||
HotIcon()
|
||||
}
|
||||
if (data.fresh) {
|
||||
TagView(
|
||||
tagText = "最新",
|
||||
modifier = Modifier
|
||||
.padding(start = 5.dp)
|
||||
.align(Alignment.Bottom)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ArrowRightListItem(
|
||||
iconRes: Any,
|
||||
title: String,
|
||||
msgCount: Int? = null,
|
||||
valueText: String = "",
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(60.dp)
|
||||
.padding(horizontal = 10.dp)
|
||||
.clickable {
|
||||
onClick.invoke()
|
||||
}
|
||||
) {
|
||||
|
||||
when (iconRes) {
|
||||
is Painter -> {
|
||||
Icon(
|
||||
painter = iconRes,
|
||||
contentDescription = null,
|
||||
tint = AppTheme.colors.icon,
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(end = 10.dp)
|
||||
)
|
||||
}
|
||||
is ImageVector -> {
|
||||
Icon(
|
||||
imageVector = iconRes,
|
||||
contentDescription = null,
|
||||
tint = AppTheme.colors.icon,
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(end = 10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.weight(1f)
|
||||
) {
|
||||
TextContent(text = title, modifier = Modifier.align(Alignment.CenterVertically))
|
||||
if (msgCount != null) {
|
||||
Text(
|
||||
text = "($msgCount)",
|
||||
fontSize = H7,
|
||||
color = AppTheme.colors.error,
|
||||
modifier = Modifier.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (valueText.isNotEmpty()) {
|
||||
TextContent(
|
||||
text = valueText,
|
||||
modifier = Modifier
|
||||
.padding(end = 5.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.KeyboardArrowRight,
|
||||
null,
|
||||
tint = AppTheme.colors.textSecondary,
|
||||
modifier = Modifier.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
|
||||
|
||||
fun getAuthorName(data: Article?): String {
|
||||
return if (data?.shareUser.isNullOrEmpty()) {
|
||||
data?.author ?: ""
|
||||
} else {
|
||||
data?.shareUser ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
fun getFirstCharFromName(data: Article?): String {
|
||||
val author = getAuthorName(data)
|
||||
return if (author.isNotEmpty()) author.trim().substring(0, 1) else "?"
|
||||
}
|
||||
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package com.zj.wanandroid.ui.widgets
|
||||
|
||||
import android.R
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults.buttonColors
|
||||
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
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
fun <T : Any> RefreshList(
|
||||
lazyPagingItems: LazyPagingItems<T>,
|
||||
isRefreshing: Boolean = false,
|
||||
onRefresh: (() -> Unit) = {},
|
||||
listState: LazyListState = rememberLazyListState(),
|
||||
itemContent: LazyListScope.() -> Unit,
|
||||
) {
|
||||
val rememberSwipeRefreshState = rememberSwipeRefreshState(isRefreshing = false)
|
||||
//错误页
|
||||
val err = lazyPagingItems.loadState.refresh is LoadState.Error
|
||||
if (err) {
|
||||
ErrorContent { lazyPagingItems.retry() }
|
||||
return
|
||||
}
|
||||
SwipeRefresh(
|
||||
state = rememberSwipeRefreshState,
|
||||
onRefresh = {
|
||||
onRefresh.invoke()
|
||||
lazyPagingItems.refresh()
|
||||
}
|
||||
) {
|
||||
//刷新状态
|
||||
rememberSwipeRefreshState.isRefreshing =
|
||||
((lazyPagingItems.loadState.refresh is LoadState.Loading) || isRefreshing)
|
||||
//列表
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
state = listState
|
||||
) {
|
||||
//条目布局
|
||||
itemContent()
|
||||
//加载更多状态:加载中和加载错误,没有更多
|
||||
if (!rememberSwipeRefreshState.isRefreshing) {
|
||||
item {
|
||||
lazyPagingItems.apply {
|
||||
when (loadState.append) {
|
||||
is LoadState.Loading -> LoadingItem()
|
||||
is LoadState.Error -> ErrorItem { retry() }
|
||||
is LoadState.NotLoading -> {
|
||||
if (loadState.append.endOfPaginationReached) {
|
||||
NoMoreItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorContent(retry: () -> Unit) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.stat_notify_error),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(Color.Red),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Text(
|
||||
text = "请求出错啦",
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(top = 10.dp)
|
||||
)
|
||||
Button(
|
||||
onClick = { retry() },
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(10.dp),
|
||||
colors = buttonColors(backgroundColor = AppTheme.colors.themeUi)
|
||||
) {
|
||||
Text(text = "重试")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorItem(retry: () -> Unit) {
|
||||
Button(
|
||||
onClick = { retry() },
|
||||
modifier = Modifier.padding(10.dp),
|
||||
colors = buttonColors(backgroundColor = AppTheme.colors.themeUi)
|
||||
) {
|
||||
Text(text = "重试")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoMoreItem() {
|
||||
Text(
|
||||
text = "没有更多了",
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadingItem() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(60.dp), contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = AppTheme.colors.themeUi,
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.height(50.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.zj.wanandroid.ui.widgets
|
||||
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.material.Snackbar
|
||||
import androidx.compose.material.SnackbarData
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
const val SNACK_INFO = ""
|
||||
const val SNACK_WARN = " "
|
||||
const val SNACK_ERROR = " "
|
||||
const val SNACK_SUCCESS = "OK"
|
||||
|
||||
@Composable
|
||||
fun AppSnackBar(data: SnackbarData) {
|
||||
Snackbar(
|
||||
snackbarData = data,
|
||||
backgroundColor = when (data.actionLabel) {
|
||||
SNACK_INFO -> AppTheme.colors.themeUi
|
||||
SNACK_WARN -> AppTheme.colors.warn
|
||||
SNACK_ERROR -> AppTheme.colors.error
|
||||
SNACK_SUCCESS -> AppTheme.colors.success
|
||||
else -> AppTheme.colors.themeUi
|
||||
},
|
||||
actionColor = AppTheme.colors.textPrimary,
|
||||
contentColor = AppTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
|
||||
fun popupSnackBar(
|
||||
scope: CoroutineScope,
|
||||
scaffoldState: ScaffoldState,
|
||||
label: String,
|
||||
message: String,
|
||||
onDismissCallback: () -> Unit = {}
|
||||
) {
|
||||
scope.launch {
|
||||
scaffoldState.snackbarHostState.showSnackbar(actionLabel = label, message = message)
|
||||
onDismissCallback.invoke()
|
||||
}
|
||||
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
package com.zj.wanandroid.ui.widgets
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.accompanist.placeholder.material.placeholder
|
||||
import com.zj.wanandroid.R
|
||||
import com.zj.wanandroid.theme.AppTheme
|
||||
import com.zj.wanandroid.theme.white
|
||||
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun HotIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_hot),
|
||||
contentDescription = null,
|
||||
tint = AppTheme.colors.hot,
|
||||
modifier = modifier
|
||||
.size(20.dp)
|
||||
.pointerInteropFilter { false }
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun ShareIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_share),
|
||||
contentDescription = null,
|
||||
modifier = modifier
|
||||
.width(25.dp)
|
||||
.height(25.dp)
|
||||
.pointerInteropFilter { false }
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun FavouriteIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
isFavourite: Boolean = false,
|
||||
onClick: () -> Unit,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isFavourite && !isLoading) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
|
||||
contentDescription = null,
|
||||
tint = if (isFavourite && !isLoading) AppTheme.colors.themeUi else AppTheme.colors.textSecondary,
|
||||
modifier = modifier
|
||||
.width(25.dp)
|
||||
.height(25.dp)
|
||||
.clickable(enabled = !isLoading) { onClick.invoke() }
|
||||
.pointerInteropFilter { false }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TimerIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_time),
|
||||
contentDescription = "",
|
||||
tint = AppTheme.colors.textSecondary,
|
||||
modifier = modifier
|
||||
.width(15.dp)
|
||||
.height(15.dp)
|
||||
.clip(RoundedCornerShape(15.dp / 2))
|
||||
.placeholder(
|
||||
visible = isLoading,
|
||||
color = AppTheme.colors.placeholder
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_author),
|
||||
contentDescription = "",
|
||||
modifier = modifier
|
||||
.width(15.dp)
|
||||
.height(15.dp)
|
||||
.clip(RoundedCornerShape(15.dp / 2))
|
||||
.placeholder(
|
||||
visible = isLoading,
|
||||
color = AppTheme.colors.placeholder
|
||||
),
|
||||
tint = AppTheme.colors.textSecondary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddIcon(
|
||||
modifier: Modifier,
|
||||
color: Color = AppTheme.colors.textPrimary
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationIcon(
|
||||
modifier: Modifier,
|
||||
tintColor: Color = white
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Notifications,
|
||||
contentDescription = "New message",
|
||||
modifier = modifier,
|
||||
tint = tintColor
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DotView(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = "",
|
||||
modifier = modifier
|
||||
.size(10.dp)
|
||||
.background(color = AppTheme.colors.hot, RoundedCornerShape(5.dp)),
|
||||
color = white,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
fontSize = 5.sp
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun DeleteIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = AppTheme.colors.textSecondary,
|
||||
modifier = modifier
|
||||
.clickable {
|
||||
onClick.invoke()
|
||||
}
|
||||
.pointerInteropFilter { false }
|
||||
)
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
package com.zj.wanandroid.ui.widgets
|
||||
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import com.google.accompanist.placeholder.material.placeholder
|
||||
import com.zj.wanandroid.theme.*
|
||||
|
||||
@Composable
|
||||
fun LargeTitle(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color? = null,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
Title(
|
||||
title = title,
|
||||
modifier = modifier,
|
||||
fontSize = H3,
|
||||
color = color ?: AppTheme.colors.textPrimary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainTitle(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
maxLine: Int = 1,
|
||||
textAlign: TextAlign = TextAlign.Start,
|
||||
color: Color = AppTheme.colors.textPrimary,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
Title(
|
||||
title = title,
|
||||
modifier = modifier,
|
||||
fontSize = H4,
|
||||
color = color,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLine = maxLine,
|
||||
textAlign = textAlign,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MediumTitle(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = AppTheme.colors.textPrimary,
|
||||
textAlign: TextAlign = TextAlign.Start,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
Title(
|
||||
title = title,
|
||||
fontSize = H5,
|
||||
modifier = modifier,
|
||||
color = color,
|
||||
textAlign = textAlign,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextContent(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = AppTheme.colors.textSecondary,
|
||||
maxLines: Int = 99,
|
||||
textAlign: TextAlign = TextAlign.Start,
|
||||
canCopy: Boolean = false,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
if (canCopy) {
|
||||
SelectionContainer {
|
||||
Title(
|
||||
title = text,
|
||||
modifier = modifier,
|
||||
fontSize = H6,
|
||||
color = color,
|
||||
maxLine = maxLines,
|
||||
textAlign = textAlign,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Title(
|
||||
title = text,
|
||||
modifier = modifier,
|
||||
fontSize = H6,
|
||||
color = color,
|
||||
maxLine = maxLines,
|
||||
textAlign = textAlign,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MiniTitle(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = AppTheme.colors.textSecondary,
|
||||
maxLines: Int = 1,
|
||||
textAlign: TextAlign = TextAlign.Start,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
Title(
|
||||
title = text,
|
||||
modifier = modifier,
|
||||
fontSize = H7,
|
||||
color = color,
|
||||
maxLine = maxLines,
|
||||
textAlign = textAlign,
|
||||
isLoading = isLoading,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Title(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
fontSize: TextUnit,
|
||||
color: Color = AppTheme.colors.textSecondary,
|
||||
fontWeight: FontWeight = FontWeight.Normal,
|
||||
maxLine: Int = 1,
|
||||
textAlign: TextAlign = TextAlign.Start,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = modifier
|
||||
.placeholder(
|
||||
visible = isLoading,
|
||||
color = AppTheme.colors.placeholder
|
||||
),
|
||||
fontSize = fontSize,
|
||||
color = color,
|
||||
maxLines = maxLine,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = textAlign
|
||||
)
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.zj.wanandroid.utils
|
||||
|
||||
import com.zj.wanandroid.data.bean.UserInfo
|
||||
import com.zj.wanandroid.data.store.DataStoreUtils
|
||||
|
||||
object AppUserUtil {
|
||||
private const val LOGGED_FLAG = "logged_flag"
|
||||
private const val USER_INFO = "user_info"
|
||||
var isLogged: Boolean
|
||||
get() = DataStoreUtils.readBooleanData(LOGGED_FLAG, false)
|
||||
set(value) = DataStoreUtils.saveSyncBooleanData(LOGGED_FLAG, value = value)
|
||||
|
||||
var userInfo: UserInfo?
|
||||
get() = DataStoreUtils.readStringData(USER_INFO).fromJson()
|
||||
set(value) = DataStoreUtils.saveSyncStringData(USER_INFO, value = value?.toJson() ?: "")
|
||||
|
||||
fun onLogin(userInfo: UserInfo) {
|
||||
isLogged = true
|
||||
this.userInfo = userInfo
|
||||
}
|
||||
|
||||
fun onLogOut() {
|
||||
isLogged = false
|
||||
this.userInfo = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.zj.wanandroid.utils
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.zj.wanandroid.utils
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.Gson
|
||||
|
||||
fun Parcelable.toJson(): String {
|
||||
return Gson().toJson(this)
|
||||
}
|
||||
|
||||
inline fun <reified T> String.fromJson(): T? {
|
||||
return try {
|
||||
Gson().fromJson(this, T::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.zj.wanandroid.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
|
||||
/**
|
||||
* 网络状态
|
||||
*/
|
||||
|
||||
object NetCheckUtil {
|
||||
|
||||
fun checkNet(context: Context): Boolean {
|
||||
// 判断是否具有可以用于通信渠道
|
||||
val mobileConnection = isMobileConnection(context)
|
||||
val wifiConnection = isWIFIConnection(context)
|
||||
return !(!mobileConnection && !wifiConnection)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断手机接入点(APN)是否处于可以使用的状态
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
private fun isMobileConnection(context: Context): Boolean {
|
||||
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val networkInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE)
|
||||
return networkInfo != null && networkInfo.isConnected
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前wifi是否是处于可以使用状态
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
private fun isWIFIConnection(context: Context): Boolean {
|
||||
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val networkInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
|
||||
return networkInfo != null && networkInfo.isConnected
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.zj.wanandroid.utils
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
class RegexUtils {
|
||||
|
||||
fun symbolClear(text: String?): String {
|
||||
// val pattern = Pattern.compile()
|
||||
if (text.isNullOrEmpty()) {
|
||||
return ""
|
||||
}
|
||||
val regex = Regex("<[a-z]+>|</[a-z]+>|<[a-z]+/>")
|
||||
if (text.contains(regex)) {
|
||||
return text.replace(regex, "")
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
fun timestamp(time: String?): String? {
|
||||
time ?: return null
|
||||
return kotlin.runCatching {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm").parse(time)
|
||||
time.substring(0, time.indexOf(" "))
|
||||
}.getOrDefault(time)
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package com.zj.wanandroid.utils
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
|
||||
/**
|
||||
* 路由名称
|
||||
*/
|
||||
object RouteUtils {
|
||||
|
||||
const val STEAD_SYMBOL = "^0^"
|
||||
|
||||
//初始化Bundle参数
|
||||
fun initBundle(params: Parcelable) = Bundle().apply { putParcelable(ARGS, params) }
|
||||
|
||||
/**
|
||||
* 导航到某个页面
|
||||
*/
|
||||
fun navTo(
|
||||
navCtrl: NavHostController,
|
||||
destinationName: String,
|
||||
args: Any? = null,
|
||||
backStackRouteName: String? = null,
|
||||
isLaunchSingleTop: Boolean = true,
|
||||
needToRestoreState: Boolean = true,
|
||||
) {
|
||||
|
||||
var singleArgument = ""
|
||||
if (args != null) {
|
||||
when (args) {
|
||||
is Parcelable -> {
|
||||
singleArgument = String.format("/%s", Uri.encode(args.toJson()))
|
||||
}
|
||||
is String -> {
|
||||
singleArgument = String.format("/%s", args)
|
||||
}
|
||||
is Int -> {
|
||||
singleArgument = String.format("/%s", args)
|
||||
}
|
||||
is Float -> {
|
||||
singleArgument = String.format("/%s", args)
|
||||
}
|
||||
is Double -> {
|
||||
singleArgument = String.format("/%s", args)
|
||||
}
|
||||
is Boolean -> {
|
||||
singleArgument = String.format("/%s", args)
|
||||
}
|
||||
is Long -> {
|
||||
singleArgument = String.format("/%s", args)
|
||||
}
|
||||
}
|
||||
}
|
||||
println("导航到: $destinationName")
|
||||
navCtrl.navigate("$destinationName$singleArgument") {
|
||||
if (backStackRouteName != null) {
|
||||
popUpTo(backStackRouteName) { saveState = true }
|
||||
}
|
||||
launchSingleTop = isLaunchSingleTop
|
||||
restoreState = needToRestoreState
|
||||
}
|
||||
}
|
||||
|
||||
fun NavHostController.back() {
|
||||
navigateUp()
|
||||
}
|
||||
|
||||
private fun getPopUpId(navCtrl: NavHostController, routeName: String?): Int {
|
||||
val defaultId = navCtrl.graph.findStartDestination().id
|
||||
return if (routeName == null) {
|
||||
defaultId
|
||||
} else {
|
||||
navCtrl.findDestination(routeName)?.id ?: defaultId
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> getArguments(navCtrl: NavHostController): T? {
|
||||
return navCtrl.previousBackStackEntry?.arguments?.getParcelable(ARGS)
|
||||
}
|
||||
|
||||
/**
|
||||
* 各个序列化的参数类的key名
|
||||
*/
|
||||
private const val ARGS = "args"
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.zj.wanandroid.utils
|
||||
|
||||
import android.content.res.Resources
|
||||
|
||||
object SizeUtils {
|
||||
fun dp2px(dpValue: Float): Int {
|
||||
val scale = Resources.getSystem().displayMetrics.density
|
||||
return (dpValue * scale + 0.5f).toInt()
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package com.zj.wanandroid.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.TextUtils
|
||||
import android.widget.Toast
|
||||
import com.zj.wanandroid.MyApp
|
||||
|
||||
private var mToast: Toast? = null
|
||||
|
||||
/**
|
||||
* 显示时间较短的吐司
|
||||
*
|
||||
* @param text String,显示的内容
|
||||
*/
|
||||
fun showToast(text: String?) {
|
||||
showToast(context = MyApp.CONTEXT, text = text)
|
||||
}
|
||||
|
||||
fun showToast(context: Context = MyApp.CONTEXT, text: String?) {
|
||||
if (TextUtils.isEmpty(text)) return
|
||||
if (Thread.currentThread() === Looper.getMainLooper().thread) {
|
||||
showToast(context, text, Toast.LENGTH_SHORT)
|
||||
} else {
|
||||
Handler(context.mainLooper).post { showToast(context, text, Toast.LENGTH_SHORT) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示时间较短的吐司
|
||||
*
|
||||
* @param resId int,显示内容的字符串索引
|
||||
*/
|
||||
fun showToast(context: Context = MyApp.CONTEXT, resId: Int) {
|
||||
if (Thread.currentThread() === Looper.getMainLooper().thread) {
|
||||
showToast(context, resId, Toast.LENGTH_SHORT)
|
||||
} else {
|
||||
Handler(context.mainLooper).post { showToast(context, resId, Toast.LENGTH_SHORT) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示时间较长的吐司
|
||||
*
|
||||
* @param text String,显示的内容
|
||||
*/
|
||||
fun showLongToast(context: Context? = MyApp.CONTEXT, text: String?) {
|
||||
if (context == null || TextUtils.isEmpty(text)) return
|
||||
if (Thread.currentThread() === Looper.getMainLooper().thread) {
|
||||
showToast(context, text, Toast.LENGTH_LONG)
|
||||
} else {
|
||||
Handler(context.mainLooper).post { showToast(context, text, Toast.LENGTH_LONG) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示时间较长的吐司
|
||||
*
|
||||
* @param resId int,显示内容的字符串索引
|
||||
*/
|
||||
fun showLongToast(context: Context? = MyApp.CONTEXT, resId: Int) {
|
||||
if (context == null) return
|
||||
if (Thread.currentThread() === Looper.getMainLooper().thread) {
|
||||
showToast(context, resId, Toast.LENGTH_LONG)
|
||||
} else {
|
||||
Handler(context.mainLooper).post { showToast(context, resId, Toast.LENGTH_LONG) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showToast(context: Context? = MyApp.CONTEXT, text: String?, duration: Int) {
|
||||
if (TextUtils.isEmpty(text)) return
|
||||
cancelToast()
|
||||
if (mToast == null) {
|
||||
mToast = Toast.makeText(context, null as CharSequence?, duration)
|
||||
}
|
||||
mToast?.apply {
|
||||
setText(text)
|
||||
this.duration = duration
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
fun showToast(context: Context? = MyApp.CONTEXT, res: Int, duration: Int) {
|
||||
cancelToast()
|
||||
if (mToast == null) {
|
||||
mToast = Toast.makeText(context, res, duration)
|
||||
} else {
|
||||
mToast?.setText(res)
|
||||
mToast?.duration = duration
|
||||
}
|
||||
mToast?.show()
|
||||
}
|
||||
|
||||
fun cancelToast() {
|
||||
mToast?.cancel()
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<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:startY="49.59793"
|
||||
android:startX="42.9492"
|
||||
android:endY="92.4963"
|
||||
android:endX="85.84757"
|
||||
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: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:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background">
|
||||
<shape>
|
||||
<solid android:color="@color/grey_cd" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<shape>
|
||||
<solid android:color="?colorPrimaryDark" />
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector android:height="32dp" android:viewportHeight="165"
|
||||
android:viewportWidth="165" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#000000" android:fillType="nonZero"
|
||||
android:pathData="M79.967,12.11C41.726,13.238 11.475,44.858 12.039,83.112C12.603,121.365 43.774,152.08 82.031,152.08C120.289,152.08 151.46,121.365 152.024,83.112C152.588,44.858 122.336,13.238 84.095,12.11L82.031,12.078L79.967,12.11ZM84.287,0.11C128.527,1.31 164.031,37.55 164.031,82.078C164.031,127.366 127.319,164.078 82.031,164.078C36.743,164.078 0.031,127.366 0.031,82.078C0.031,37.542 35.527,1.302 79.775,0.11L82.031,0.078L84.287,0.11Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#000000" android:fillType="nonZero"
|
||||
android:pathData="M82.031,42.438C85.343,42.438 88.031,45.766 88.031,49.878L88.031,114.278C88.031,118.39 85.343,121.718 82.031,121.718C78.719,121.718 76.031,118.39 76.031,114.278L76.031,49.878C76.031,45.766 78.719,42.438 82.031,42.438Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#000000" android:fillType="nonZero"
|
||||
android:pathData="M42.391,82.078C42.391,78.766 45.719,76.078 49.831,76.078L114.231,76.078C118.343,76.078 121.671,78.766 121.671,82.078C121.671,85.39 118.343,88.078 114.231,88.078L49.831,88.078C45.719,88.078 42.391,85.39 42.391,82.078Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,4 @@
|
||||
<vector android:height="32dp" android:viewportHeight="1024"
|
||||
android:viewportWidth="1024" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M268.37,908.63l60.59,60.07 456.7,-460.37L328.7,55.04l-60.08,60.59 396.37,393.22z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M895.95,734.05l1.07,1.01a29.82,29.82 0,0 1,0 43.41l-162.26,152.96a31.93,31.93 0,0 1,-22.76 8.7,31.93 31.93,0 0,1 -22.77,-8.7l-93.18,-87.84a29.82,29.82 0,0 1,0 -43.41l1.08,-1.01a32,32 0,0 1,43.9 0l70.98,66.9 140.05,-132.02a32,32 0,0 1,43.9 0zM768,85.33c64.8,0 117.33,52.53 117.33,117.33v394.67a32,32 0,0 1,-64 0L821.33,202.67a53.33,53.33 0,0 0,-53.33 -53.33L256,149.33a53.33,53.33 0,0 0,-53.33 53.33v618.67a53.33,53.33 0,0 0,53.33 53.33h234.67a32,32 0,0 1,0 64L256,938.67c-64.8,0 -117.33,-52.53 -117.33,-117.33L138.67,202.67c0,-64.8 52.53,-117.33 117.33,-117.33zM554.67,544a32,32 0,0 1,0 64L341.33,608a32,32 0,0 1,0 -64zM682.67,373.33a32,32 0,0 1,0 64L341.33,437.33a32,32 0,0 1,0 -64z"
|
||||
android:fillColor="#e6e6e6"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22 0.03,-1.99 4,-3.08 6,-3.08 1.99,0 5.97,1.09 6,3.08 -1.29,1.94 -3.5,3.22 -6,3.22z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M659.93,128a74.67,74.67 0,0 1,71.34 52.62L754.56,256L821.33,256c64.8,0 117.33,52.53 117.33,117.33v426.67c0,64.8 -52.53,117.33 -117.33,117.33L202.67,917.33c-64.8,0 -117.33,-52.53 -117.33,-117.33L85.33,373.33c0,-64.8 52.53,-117.33 117.33,-117.33h66.77l23.3,-75.38A74.67,74.67 0,0 1,364.07 128h295.85zM512,405.33c-88.36,0 -160,71.64 -160,160 0,88.36 71.64,160 160,160 88.36,0 160,-71.64 160,-160 0,-88.36 -71.64,-160 -160,-160zM512,661.33a96,96 0,1 0,0 -192,96 96,0 0,0 0,192z"
|
||||
android:fillColor="#e6e6e6"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M512,938.67C276.36,938.67 85.33,747.64 85.33,512S276.36,85.33 512,85.33s426.67,191.03 426.67,426.67 -191.03,426.67 -426.67,426.67zM512,874.67c200.3,0 362.67,-162.37 362.67,-362.67S712.3,149.33 512,149.33 149.33,311.7 149.33,512s162.37,362.67 362.67,362.67zM368.86,684.31a32,32 0,1 1,40.92 -49.21A159.19,159.19 0,0 0,512 672c37.89,0 73.67,-13.17 102.19,-36.89a32,32 0,0 1,40.92 49.22A223.18,223.18 0,0 1,512 736a223.18,223.18 0,0 1,-143.14 -51.69z"
|
||||
android:fillColor="#e6e6e6"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M512,938.67C276.36,938.67 85.33,747.64 85.33,512S276.36,85.33 512,85.33s426.67,191.03 426.67,426.67 -191.03,426.67 -426.67,426.67zM512,874.67c200.3,0 362.67,-162.37 362.67,-362.67S712.3,149.33 512,149.33 149.33,311.7 149.33,512s162.37,362.67 362.67,362.67zM520.21,608.99l-81.43,-88.7 -95.75,99.26a32,32 0,1 1,-46.06 -44.44l119.36,-123.73a32,32 0,0 1,46.61 0.58l81.79,89.11 136.61,-136.99a32,32 0,1 1,45.31 45.18l-160.21,160.68a32,32 0,0 1,-46.24 -0.96z"
|
||||
android:fillColor="#e6e6e6"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M202.67,256h-42.67a32,32 0,0 1,0 -64h704a32,32 0,0 1,0 64L266.67,256v565.33a53.33,53.33 0,0 0,53.33 53.33h384a53.33,53.33 0,0 0,53.33 -53.33L757.33,352a32,32 0,0 1,64 0v469.33c0,64.8 -52.53,117.33 -117.33,117.33L320,938.67c-64.8,0 -117.33,-52.53 -117.33,-117.33L202.67,256zM426.67,149.33a32,32 0,0 1,0 -64h170.67a32,32 0,0 1,0 64L426.67,149.33zM394.67,437.33a32,32 0,0 1,64 0v256a32,32 0,0 1,-64 0L394.67,437.33zM565.33,437.33a32,32 0,0 1,64 0v256a32,32 0,0 1,-64 0L565.33,437.33z"
|
||||
android:fillColor="#e6e6e6"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M896,170.67v85.33L128,256L128,170.67h768zM896,469.33v85.33L128,554.67v-85.33h768zM896,768v85.33L128,853.33v-85.33h768z"
|
||||
android:fillColor="#cdcdcd"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M778.67,117.33c64.8,0 117.33,52.53 117.33,117.33v640c0,25.56 -28.48,40.8 -49.75,26.62l-77.16,-51.43L708.27,895.47a42.67,42.67 0,0 1,-51.2 0l-59.73,-44.8 -59.73,44.8a42.67,42.67 0,0 1,-51.2 0l-59.73,-44.8 -59.73,44.8a42.67,42.67 0,0 1,-51.2 0l-60.82,-45.61 -77.16,51.43C156.48,915.48 128,900.22 128,874.67L128,234.67c0,-64.8 52.53,-117.33 117.33,-117.33h533.33zM778.67,181.33L245.33,181.33a53.33,53.33 0,0 0,-53.33 53.33v580.2l39.97,-26.65a42.67,42.67 0,0 1,49.27 1.38L341.33,834.67l59.73,-44.8a42.67,42.67 0,0 1,51.2 0l59.73,44.8 59.73,-44.8a42.67,42.67 0,0 1,51.2 0l59.73,44.8 60.1,-45.07a42.67,42.67 0,0 1,49.27 -1.38L832,814.87L832,234.67a53.33,53.33 0,0 0,-53.33 -53.33zM565.33,522.67a32,32 0,0 1,0 64L352,586.67a32,32 0,0 1,0 -64zM672,352a32,32 0,0 1,0 64L352,416a32,32 0,0 1,0 -64z"
|
||||
android:fillColor="#e6e6e6"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M878.08,731.27a32,32 0,0 1,-54.88 -32.94A360.79,360.79 0,0 0,874.67 512c0,-200.3 -162.37,-362.67 -362.67,-362.67S149.33,311.7 149.33,512s162.37,362.67 362.67,362.67a360.79,360.79 0,0 0,186.31 -51.45,32 32,0 0,1 32.93,54.88A424.78,424.78 0,0 1,512 938.67C276.36,938.67 85.33,747.64 85.33,512S276.36,85.33 512,85.33s426.67,191.03 426.67,426.67c0,78.29 -21.15,153.57 -60.59,219.27zM650.67,437.33c0,65.9 -46.72,120.85 -109.19,135.08V608a32,32 0,0 1,-64 0v-64a32,32 0,0 1,32 -32C552.27,512 586.67,478.4 586.67,437.33s-34.4,-74.67 -77.19,-74.67c-26.77,0 -51.08,13.25 -65.17,34.62a73.09,73.09 0,0 0,-8.52 17.72,32 32,0 0,1 -60.89,-19.69c3.8,-11.75 9.17,-22.93 15.98,-33.24 25.86,-39.25 70.19,-63.41 118.61,-63.41C587.27,298.67 650.67,360.58 650.67,437.33zM512,736a32,32 0,1 1,0 -64,32 32,0 0,1 0,64z"
|
||||
android:fillColor="#e6e6e6"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M822.5,473.15l52.05,29.29C869.46,306.56 709.1,149.33 512,149.33c-200.3,0 -362.67,162.37 -362.67,362.67s162.37,362.67 362.67,362.67c122.54,0 234.65,-61.19 301.58,-161.15a32,32 0,1 1,53.17 35.62C788.06,866.63 656.12,938.67 512,938.67 276.36,938.67 85.33,747.64 85.33,512S276.36,85.33 512,85.33s426.67,191.03 426.67,426.67c0,10.95 -0.85,26.36 -2.52,46.53 -1.93,23.24 -27.27,36.68 -47.59,25.25l-97.45,-54.85a32,32 0,1 1,31.39 -55.79zM329.38,649.37L480,498.76L480,320a32,32 0,0 1,64 0v192a32,32 0,0 1,-9.38 22.62l-160,160a32,32 0,1 1,-45.25 -45.25z"
|
||||
android:fillColor="#e6e6e6"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M423.48,938.67S45.05,855.42 214.19,442.28c0,0 38.4,45.91 33.12,68 0,0 30.1,-104.28 95.07,-166.57C398.17,290.19 454.85,139.71 402.57,85.33c0,0 258.93,54.38 287.75,326.38 0,0 33.12,-86.67 101.12,-95.23 0,0 -20.91,47.62 0,119.04 0,0 214.49,367.15 -155.16,491.24 0,0 110.81,-125.81 -124.18,-341.72 0,0 -55.4,115.63 -88.53,156.37 -0.1,0.11 -92.52,103.72 -0.1,197.25z"
|
||||
android:fillColor="#c51614"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M20,6h-2.18c0.11,-0.31 0.18,-0.65 0.18,-1 0,-1.66 -1.34,-3 -3,-3 -1.05,0 -1.96,0.54 -2.5,1.35l-0.5,0.67 -0.5,-0.68C10.96,2.54 10.05,2 9,2 7.34,2 6,3.34 6,5c0,0.35 0.07,0.69 0.18,1L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM15,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM9,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM20,19L4,19v-2h16v2zM20,14L4,14L4,8h5.08L7,10.83 8.62,12 11,8.76l1,-1.36 1,1.36L15.38,12 17,10.83 14.92,8L20,8v6z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M821.33,800H547.58l-86.46,96.07a32,32 0,1 1,-47.57 -42.82l96,-106.67A32,32 0,0 1,533.33 736h288a53.33,53.33 0,0 0,53.33 -53.33V234.67a53.33,53.33 0,0 0,-53.33 -53.33H202.67a53.33,53.33 0,0 0,-53.33 53.33v448a53.33,53.33 0,0 0,53.33 53.33h138.67a32,32 0,0 1,0 64H202.67c-64.8,0 -117.33,-52.53 -117.33,-117.33V234.67c0,-64.8 52.53,-117.33 117.33,-117.33h618.67c64.8,0 117.33,52.53 117.33,117.33v448c0,64.8 -52.53,117.33 -117.33,117.33zM704,341.33a32,32 0,0 1,0 64H320a32,32 0,0 1,0 -64h384zM512,512a32,32 0,0 1,0 64H320a32,32 0,0 1,0 -64h192z"
|
||||
android:fillColor="#e6e6e6"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M544,661.33a32,32 0,0 1,-64 0L480,362.67a32,32 0,0 1,64 0v298.67zM704,661.33a32,32 0,0 1,-64 0L640,490.67a32,32 0,0 1,64 0v170.67zM384,661.33a32,32 0,0 1,-64 0L320,448a32,32 0,0 1,64 0v213.33zM202.67,138.67h618.67c64.8,0 117.33,52.53 117.33,117.33v512c0,64.8 -52.53,117.33 -117.33,117.33L202.67,885.33c-64.8,0 -117.33,-52.53 -117.33,-117.33L85.33,256c0,-64.8 52.53,-117.33 117.33,-117.33zM202.67,202.67a53.33,53.33 0,0 0,-53.33 53.33v512a53.33,53.33 0,0 0,53.33 53.33h618.67a53.33,53.33 0,0 0,53.33 -53.33L874.67,256a53.33,53.33 0,0 0,-53.33 -53.33L202.67,202.67z"
|
||||
android:fillColor="#e6e6e6"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M797.53,752.27c62.07,-72.74 97.28,-165 97.28,-262.19C894.82,266.53 713.62,85.33 490.08,85.33 266.54,85.33 85.33,266.54 85.33,490.07 85.33,713.61 266.54,894.83 490.07,894.83a404.69,404.69 0,0 0,118.21 -17.55,32 32,0 0,0 -18.67,-61.22 340.69,340.69 0,0 1,-99.54 14.76C301.89,830.82 149.33,678.26 149.33,490.07 149.33,301.89 301.89,149.33 490.07,149.33 678.26,149.33 830.83,301.89 830.83,490.07c0,89.28 -35.38,173.7 -97.14,237.32a36.99,36.99 0,0 0,0.38 51.93l149.97,149.97a32,32 0,0 0,45.26 -45.25L797.53,752.27z"
|
||||
android:fillColor="@color/white"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user