├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── me
│ │ └── rerere
│ │ └── zhiwang
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── me
│ │ │ └── rerere
│ │ │ └── zhiwang
│ │ │ ├── AppContext.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── api
│ │ │ ├── bilibili
│ │ │ │ ├── BilibiliUser.kt
│ │ │ │ ├── BilibiliUserData.kt
│ │ │ │ ├── BilibiliUtil.kt
│ │ │ │ ├── SubList.kt
│ │ │ │ └── SubResult.kt
│ │ │ ├── wiki
│ │ │ │ ├── WikiList.kt
│ │ │ │ └── WikiUtil.kt
│ │ │ ├── zhiwang
│ │ │ │ ├── Request.kt
│ │ │ │ ├── Response.kt
│ │ │ │ └── ZhiWangService.kt
│ │ │ └── zuowen
│ │ │ │ ├── ZuowenContent.kt
│ │ │ │ ├── ZuowenPageSource.kt
│ │ │ │ ├── ZuowenResponse.kt
│ │ │ │ └── ZuowenService.kt
│ │ │ ├── di
│ │ │ └── NetworkModule.kt
│ │ │ ├── repo
│ │ │ ├── WikiRepo.kt
│ │ │ └── ZuowenRepo.kt
│ │ │ ├── ui
│ │ │ ├── public
│ │ │ │ └── XiaoZuoWen.kt
│ │ │ ├── screen
│ │ │ │ ├── index
│ │ │ │ │ ├── IndexScreen.kt
│ │ │ │ │ ├── IndexScreenVideoModel.kt
│ │ │ │ │ └── page
│ │ │ │ │ │ ├── About.kt
│ │ │ │ │ │ ├── Chachong.kt
│ │ │ │ │ │ ├── MemeWiki.kt
│ │ │ │ │ │ └── Zuowen.kt
│ │ │ │ └── zuowen
│ │ │ │ │ ├── ZuowenScreen.kt
│ │ │ │ │ └── ZuowenViewModel.kt
│ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Shape.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ └── util
│ │ │ ├── ModifierEx.kt
│ │ │ ├── ReplaceUtil.kt
│ │ │ ├── UpdateChecker.kt
│ │ │ ├── android
│ │ │ └── ClipboardUtil.kt
│ │ │ ├── charts
│ │ │ ├── ChartShape.kt
│ │ │ ├── bars
│ │ │ │ ├── DropdownContent.kt
│ │ │ │ ├── HorizontalBarsChart.kt
│ │ │ │ ├── StackedHorizontalBar.kt
│ │ │ │ ├── Utils.kt
│ │ │ │ └── data
│ │ │ │ │ ├── HorizontalBarsData.kt
│ │ │ │ │ ├── StackedBarData.kt
│ │ │ │ │ ├── StackedBarEntry.kt
│ │ │ │ │ └── StackedBarItem.kt
│ │ │ ├── internal
│ │ │ │ ├── DefaultText.kt
│ │ │ │ └── Utils.kt
│ │ │ ├── legend
│ │ │ │ ├── HorizontalLegend.kt
│ │ │ │ ├── LegendEntry.kt
│ │ │ │ └── VerticalLegend.kt
│ │ │ ├── pie
│ │ │ │ ├── LegendPosition.kt
│ │ │ │ ├── PieChart.kt
│ │ │ │ ├── PieChartData.kt
│ │ │ │ ├── PieChartEntry.kt
│ │ │ │ ├── PieChartRenderer.kt
│ │ │ │ └── Utils.kt
│ │ │ └── table
│ │ │ │ ├── Table.kt
│ │ │ │ ├── TableEntry.kt
│ │ │ │ └── TableRow.kt
│ │ │ ├── format
│ │ │ ├── Format.kt
│ │ │ └── HtmlFormatUtil.kt
│ │ │ └── net
│ │ │ ├── AutoRetry.kt
│ │ │ ├── OkhttpUtil.kt
│ │ │ └── UserAgentInterceptor.kt
│ └── res
│ │ ├── drawable-v24
│ │ ├── asoul.jpg
│ │ └── chengfen.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_rounded.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_rounded.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_rounded.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_rounded.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_rounded.png
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── me
│ └── rerere
│ └── zhiwang
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── buildSrc
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── Versions.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── screenshots
├── chengfen.png
├── query.png
└── zuowen.png
├── settings.gradle.kts
└── wiki.json
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 | /buildSrc/build
12 | /app/release
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ASoulZhiWang
2 | 一个ASOUL评论区小作文APP,可以查询小作文出处,也可以浏览小作文库
3 | * 查重API来自: https://github.com/stream2000/ASoulCnki
4 | * 小作文库来自: https://asoul.icu
5 |
6 | ## A-SOUL简介 🥵
7 | A-SOUL是乐华娱乐于2020年11月23日公开的其旗下首个虚拟偶像团体,由5名成员组成。
8 | * A-SOUL主页链接:https://space.bilibili.com/703007996
9 | * 珈乐:https://space.bilibili.com/351609538
10 | * 乃琳:https://space.bilibili.com/672342685
11 | * 贝拉:https://space.bilibili.com/672353429
12 | * 向晚:https://space.bilibili.com/672346917
13 | * 嘉然:https://space.bilibili.com/672328094
14 |
15 | 在未来学院中,五位性格迥异的少女,为了成为偶像这一共同目标走到一起,并且为之努力奋斗。
16 |
17 | ## 下载 🐭
18 | 前往Release页面下载: https://github.com/jiangdashao/ASoulZhiWang/releases
19 |
20 | ## 技术栈和引用库 👍
21 | * JetPack Compose - 声明式UI框架
22 | * Hilt - 依赖注入
23 | * Navigation - 导航
24 | * Retrofit - 访问Restful API
25 | * Paging3 - 列表数据加载库
26 |
27 | ## 截图 🎞
28 | | 查重 | 小作文库 | 查成分 |
29 | | ----- | ------| ------|
30 | |
|
|
|
31 |
32 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id ("com.android.application")
3 | id ("kotlin-android")
4 | id ("kotlin-kapt")
5 | id ("dagger.hilt.android.plugin")
6 | }
7 |
8 | android {
9 | compileSdk = 31
10 | buildToolsVersion = "30.0.3"
11 |
12 | defaultConfig {
13 | applicationId = "me.rerere.zhiwang"
14 | minSdk = 26
15 | targetSdk = 31
16 | versionCode = 12
17 | versionName = "2.0.0"
18 |
19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary = true
22 | }
23 | }
24 |
25 | buildTypes {
26 | release {
27 | isMinifyEnabled = false
28 | proguardFiles (getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
29 | }
30 | }
31 | compileOptions {
32 | sourceCompatibility (JavaVersion.VERSION_1_8)
33 | targetCompatibility (JavaVersion.VERSION_1_8)
34 | }
35 | kotlinOptions {
36 | jvmTarget = "1.8"
37 | }
38 | buildFeatures {
39 | compose = true
40 | }
41 | composeOptions {
42 | kotlinCompilerExtensionVersion = composeVersion
43 | }
44 | }
45 |
46 | dependencies {
47 | implementation ("androidx.core:core-ktx:1.7.0")
48 | implementation ("androidx.appcompat:appcompat:1.3.1")
49 | implementation ("com.google.android.material:material:1.4.0")
50 |
51 | // Compose Lib
52 | implementation ("androidx.compose.ui:ui:$composeVersion")
53 | implementation ("androidx.compose.material:material:$composeVersion")
54 | implementation("androidx.compose.material3:material3:1.0.0-alpha01")
55 | implementation ("androidx.compose.ui:ui-tooling:$composeVersion")
56 | implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0")
57 | implementation ("androidx.activity:activity-compose:1.4.0")
58 | implementation ("androidx.compose.runtime:runtime-livedata:$composeVersion")
59 |
60 | // splash
61 | implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
62 |
63 | // Dialog
64 | implementation ("io.github.vanpra.compose-material-dialogs:core:0.6.1")
65 |
66 | // Coil
67 | implementation("io.coil-kt:coil-compose:1.3.2")
68 |
69 | // Hilt
70 | implementation ("com.google.dagger:hilt-android:$hiltVersion")
71 | kapt ("com.google.dagger:hilt-compiler:$hiltVersion")
72 | implementation ("androidx.hilt:hilt-navigation-compose:1.0.0-alpha03")
73 |
74 | // Paging3
75 | implementation ("androidx.paging:paging-runtime-ktx:3.1.0-beta01")
76 | implementation ("androidx.paging:paging-compose:1.0.0-alpha14")
77 |
78 | // 图标扩展
79 | implementation ("androidx.compose.material:material-icons-extended:$composeVersion")
80 |
81 | // Navigation for JetpackCompose
82 | implementation ("androidx.navigation:navigation-compose:2.4.0-beta01")
83 |
84 | // accompanist
85 | // Pager
86 | implementation ("com.google.accompanist:accompanist-pager:$accVersion")
87 | // Swipe to refresh
88 | implementation ("com.google.accompanist:accompanist-swiperefresh:$accVersion")
89 | // 状态栏颜色
90 | implementation ("com.google.accompanist:accompanist-systemuicontroller:$accVersion")
91 | // Insets
92 | implementation ("com.google.accompanist:accompanist-insets:$accVersion")
93 | implementation ("com.google.accompanist:accompanist-insets-ui:$accVersion")
94 | // Flow
95 | implementation ("com.google.accompanist:accompanist-flowlayout:$accVersion")
96 | // Placeholder
97 | implementation ("com.google.accompanist:accompanist-placeholder-material:$accVersion")
98 |
99 | // Retrofit
100 | implementation ("com.squareup.okhttp3:okhttp:5.0.0-alpha.2")
101 | implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
102 | implementation ("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.2")
103 |
104 | // JSOUP
105 | implementation ("org.jsoup:jsoup:1.13.1")
106 |
107 | // 约束布局
108 | implementation ("androidx.constraintlayout:constraintlayout:2.1.1")
109 | implementation ("androidx.constraintlayout:constraintlayout-compose:1.0.0-rc01")
110 |
111 |
112 | testImplementation ("junit:junit:4.+")
113 | androidTestImplementation ("androidx.test.ext:junit:1.1.3")
114 | androidTestImplementation ("androidx.test.espresso:espresso-core:3.4.0")
115 | androidTestImplementation ("androidx.compose.ui:ui-test-junit4:$composeVersion")
116 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/me/rerere/zhiwang/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("me.rerere.zhiwang", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
16 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/AppContext.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import dagger.hilt.android.HiltAndroidApp
6 |
7 | @HiltAndroidApp
8 | class AppContext : Application() {
9 | companion object {
10 | lateinit var appContext: Context
11 | }
12 |
13 | override fun onCreate() {
14 | super.onCreate()
15 | appContext = this
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang
2 |
3 | import android.os.Build
4 | import android.os.Bundle
5 | import android.view.ViewGroup
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.material.MaterialTheme
10 | import androidx.compose.runtime.SideEffect
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.platform.ComposeView
14 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
15 | import androidx.core.view.WindowCompat
16 | import androidx.navigation.NavType
17 | import androidx.navigation.compose.NavHost
18 | import androidx.navigation.compose.composable
19 | import androidx.navigation.compose.rememberNavController
20 | import androidx.navigation.navArgument
21 | import com.google.accompanist.insets.ProvideWindowInsets
22 | import com.google.accompanist.pager.ExperimentalPagerApi
23 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
24 | import dagger.hilt.android.AndroidEntryPoint
25 | import me.rerere.zhiwang.ui.screen.index.IndexScreen
26 | import me.rerere.zhiwang.ui.screen.zuowen.ZuowenScreen
27 | import me.rerere.zhiwang.ui.theme.ZhiWangTheme
28 |
29 | @AndroidEntryPoint
30 | class MainActivity : ComponentActivity() {
31 | @OptIn(ExperimentalPagerApi::class, androidx.compose.animation.ExperimentalAnimationApi::class)
32 | override fun onCreate(savedInstanceState: Bundle?) {
33 | super.onCreate(savedInstanceState)
34 |
35 | // 全屏
36 | WindowCompat.setDecorFitsSystemWindows(window, false)
37 |
38 | installSplashScreen()
39 |
40 | setContent {
41 | ZhiWangTheme {
42 | ProvideWindowInsets {
43 | val navController = rememberNavController()
44 |
45 | val systemUiController = rememberSystemUiController()
46 | val darkIcons = MaterialTheme.colors.isLight
47 |
48 | // 设置状态栏和导航栏颜色
49 | SideEffect {
50 | systemUiController.setNavigationBarColor(
51 | Color.Transparent,
52 | darkIcons = darkIcons
53 | )
54 | systemUiController.setStatusBarColor(
55 | Color.Transparent,
56 | darkIcons = darkIcons
57 | )
58 | }
59 |
60 | // 导航部件
61 | NavHost(
62 | modifier = Modifier.fillMaxSize(),
63 | navController = navController,
64 | startDestination = "index"
65 | ) {
66 | composable("index") {
67 | IndexScreen(navController)
68 | }
69 |
70 | composable("zuowen?id={id}&title={title}&author={author}&tags={tags}",
71 | arguments = listOf(
72 | navArgument("id") {
73 | type = NavType.StringType
74 | },
75 | navArgument("title") {
76 | type = NavType.StringType
77 | },
78 | navArgument("author") {
79 | type = NavType.StringType
80 | },
81 | navArgument("tags") {
82 | type = NavType.StringType
83 | }
84 | )) {
85 | ZuowenScreen(
86 | navController,
87 | it.arguments?.getString("id")!!,
88 | it.arguments?.getString("title")!!,
89 | it.arguments?.getString("author")!!,
90 | it.arguments?.getString("tags")!!.run {
91 | split(",")
92 | }
93 | )
94 | }
95 | }
96 | }
97 | }
98 | }
99 |
100 | // 禁止强制暗色模式,因为已经适配了夜间模式,所以不需要强制反色
101 | // 国产UI似乎必需这样做(isForceDarkAllowed = false)才能阻止反色,原生会自动识别
102 | val existingComposeView = window.decorView
103 | .findViewById(android.R.id.content)
104 | .getChildAt(0) as? ComposeView
105 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
106 | existingComposeView?.isForceDarkAllowed = false
107 | }
108 | }
109 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/bilibili/BilibiliUser.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.bilibili
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class BilibiliUser(
7 | @SerializedName("code")
8 | val code: Int,
9 | @SerializedName("data")
10 | val `data`: Data,
11 | @SerializedName("message")
12 | val message: String,
13 | @SerializedName("ttl")
14 | val ttl: Int
15 | ) {
16 | data class Data(
17 | @SerializedName("birthday")
18 | val birthday: String,
19 | @SerializedName("coins")
20 | val coins: Int,
21 | @SerializedName("face")
22 | val face: String,
23 | @SerializedName("fans_badge")
24 | val fansBadge: Boolean,
25 | @SerializedName("is_followed")
26 | val isFollowed: Boolean,
27 | @SerializedName("jointime")
28 | val jointime: Int,
29 | @SerializedName("level")
30 | val level: Int,
31 | @SerializedName("live_room")
32 | val liveRoom: LiveRoom,
33 | @SerializedName("mid")
34 | val mid: Int,
35 | @SerializedName("moral")
36 | val moral: Int,
37 | @SerializedName("name")
38 | val name: String,
39 | @SerializedName("nameplate")
40 | val nameplate: Nameplate,
41 | @SerializedName("official")
42 | val official: Official,
43 | @SerializedName("pendant")
44 | val pendant: Pendant,
45 | @SerializedName("rank")
46 | val rank: Int,
47 | @SerializedName("sex")
48 | val sex: String,
49 | @SerializedName("sign")
50 | val sign: String,
51 | @SerializedName("silence")
52 | val silence: Int,
53 | @SerializedName("sys_notice")
54 | val sysNotice: SysNotice,
55 | @SerializedName("theme")
56 | val theme: Theme,
57 | @SerializedName("top_photo")
58 | val topPhoto: String,
59 | @SerializedName("user_honour_info")
60 | val userHonourInfo: UserHonourInfo,
61 | @SerializedName("vip")
62 | val vip: Vip
63 | ) {
64 | data class LiveRoom(
65 | @SerializedName("broadcast_type")
66 | val broadcastType: Int,
67 | @SerializedName("cover")
68 | val cover: String,
69 | @SerializedName("liveStatus")
70 | val liveStatus: Int,
71 | @SerializedName("online")
72 | val online: Int,
73 | @SerializedName("roomStatus")
74 | val roomStatus: Int,
75 | @SerializedName("roomid")
76 | val roomid: Int,
77 | @SerializedName("roundStatus")
78 | val roundStatus: Int,
79 | @SerializedName("title")
80 | val title: String,
81 | @SerializedName("url")
82 | val url: String
83 | )
84 |
85 | data class Nameplate(
86 | @SerializedName("condition")
87 | val condition: String,
88 | @SerializedName("image")
89 | val image: String,
90 | @SerializedName("image_small")
91 | val imageSmall: String,
92 | @SerializedName("level")
93 | val level: String,
94 | @SerializedName("name")
95 | val name: String,
96 | @SerializedName("nid")
97 | val nid: Int
98 | )
99 |
100 | data class Official(
101 | @SerializedName("desc")
102 | val desc: String,
103 | @SerializedName("role")
104 | val role: Int,
105 | @SerializedName("title")
106 | val title: String,
107 | @SerializedName("type")
108 | val type: Int
109 | )
110 |
111 | data class Pendant(
112 | @SerializedName("expire")
113 | val expire: Int,
114 | @SerializedName("image")
115 | val image: String,
116 | @SerializedName("image_enhance")
117 | val imageEnhance: String,
118 | @SerializedName("image_enhance_frame")
119 | val imageEnhanceFrame: String,
120 | @SerializedName("name")
121 | val name: String,
122 | @SerializedName("pid")
123 | val pid: Int
124 | )
125 |
126 | class SysNotice(
127 | )
128 |
129 | class Theme(
130 | )
131 |
132 | data class UserHonourInfo(
133 | @SerializedName("colour")
134 | val colour: Any,
135 | @SerializedName("mid")
136 | val mid: Int,
137 | @SerializedName("tags")
138 | val tags: Any
139 | )
140 |
141 | data class Vip(
142 | @SerializedName("avatar_subscript")
143 | val avatarSubscript: Int,
144 | @SerializedName("avatar_subscript_url")
145 | val avatarSubscriptUrl: String,
146 | @SerializedName("due_date")
147 | val dueDate: Int,
148 | @SerializedName("label")
149 | val label: Label,
150 | @SerializedName("nickname_color")
151 | val nicknameColor: String,
152 | @SerializedName("role")
153 | val role: Int,
154 | @SerializedName("status")
155 | val status: Int,
156 | @SerializedName("theme_type")
157 | val themeType: Int,
158 | @SerializedName("type")
159 | val type: Int,
160 | @SerializedName("vip_pay_type")
161 | val vipPayType: Int
162 | ) {
163 | data class Label(
164 | @SerializedName("bg_color")
165 | val bgColor: String,
166 | @SerializedName("bg_style")
167 | val bgStyle: Int,
168 | @SerializedName("border_color")
169 | val borderColor: String,
170 | @SerializedName("label_theme")
171 | val labelTheme: String,
172 | @SerializedName("path")
173 | val path: String,
174 | @SerializedName("text")
175 | val text: String,
176 | @SerializedName("text_color")
177 | val textColor: String
178 | )
179 | }
180 | }
181 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/bilibili/BilibiliUserData.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.bilibili
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class BilibiliUserData(
7 | @SerializedName("code")
8 | val code: Int,
9 | @SerializedName("data")
10 | val `data`: Data,
11 | @SerializedName("message")
12 | val message: String,
13 | @SerializedName("ttl")
14 | val ttl: Int
15 | ) {
16 | data class Data(
17 | @SerializedName("birthday")
18 | val birthday: String,
19 | @SerializedName("coins")
20 | val coins: Int,
21 | @SerializedName("face")
22 | val face: String,
23 | @SerializedName("fans_badge")
24 | val fansBadge: Boolean,
25 | @SerializedName("is_followed")
26 | val isFollowed: Boolean,
27 | @SerializedName("jointime")
28 | val jointime: Int,
29 | @SerializedName("level")
30 | val level: Int,
31 | @SerializedName("live_room")
32 | val liveRoom: LiveRoom,
33 | @SerializedName("mid")
34 | val mid: Int,
35 | @SerializedName("moral")
36 | val moral: Int,
37 | @SerializedName("name")
38 | val name: String,
39 | @SerializedName("nameplate")
40 | val nameplate: Nameplate,
41 | @SerializedName("official")
42 | val official: Official,
43 | @SerializedName("pendant")
44 | val pendant: Pendant,
45 | @SerializedName("rank")
46 | val rank: Int,
47 | @SerializedName("sex")
48 | val sex: String,
49 | @SerializedName("sign")
50 | val sign: String,
51 | @SerializedName("silence")
52 | val silence: Int,
53 | @SerializedName("sys_notice")
54 | val sysNotice: SysNotice,
55 | @SerializedName("theme")
56 | val theme: Theme,
57 | @SerializedName("top_photo")
58 | val topPhoto: String,
59 | @SerializedName("user_honour_info")
60 | val userHonourInfo: UserHonourInfo,
61 | @SerializedName("vip")
62 | val vip: Vip
63 | ) {
64 | data class LiveRoom(
65 | @SerializedName("broadcast_type")
66 | val broadcastType: Int,
67 | @SerializedName("cover")
68 | val cover: String,
69 | @SerializedName("liveStatus")
70 | val liveStatus: Int,
71 | @SerializedName("online")
72 | val online: Int,
73 | @SerializedName("roomStatus")
74 | val roomStatus: Int,
75 | @SerializedName("roomid")
76 | val roomid: Int,
77 | @SerializedName("roundStatus")
78 | val roundStatus: Int,
79 | @SerializedName("title")
80 | val title: String,
81 | @SerializedName("url")
82 | val url: String
83 | )
84 |
85 | data class Nameplate(
86 | @SerializedName("condition")
87 | val condition: String,
88 | @SerializedName("image")
89 | val image: String,
90 | @SerializedName("image_small")
91 | val imageSmall: String,
92 | @SerializedName("level")
93 | val level: String,
94 | @SerializedName("name")
95 | val name: String,
96 | @SerializedName("nid")
97 | val nid: Int
98 | )
99 |
100 | data class Official(
101 | @SerializedName("desc")
102 | val desc: String,
103 | @SerializedName("role")
104 | val role: Int,
105 | @SerializedName("title")
106 | val title: String,
107 | @SerializedName("type")
108 | val type: Int
109 | )
110 |
111 | data class Pendant(
112 | @SerializedName("expire")
113 | val expire: Int,
114 | @SerializedName("image")
115 | val image: String,
116 | @SerializedName("image_enhance")
117 | val imageEnhance: String,
118 | @SerializedName("image_enhance_frame")
119 | val imageEnhanceFrame: String,
120 | @SerializedName("name")
121 | val name: String,
122 | @SerializedName("pid")
123 | val pid: Int
124 | )
125 |
126 | class SysNotice(
127 | )
128 |
129 | class Theme(
130 | )
131 |
132 | data class UserHonourInfo(
133 | @SerializedName("colour")
134 | val colour: Any,
135 | @SerializedName("mid")
136 | val mid: Int,
137 | @SerializedName("tags")
138 | val tags: Any
139 | )
140 |
141 | data class Vip(
142 | @SerializedName("avatar_subscript")
143 | val avatarSubscript: Int,
144 | @SerializedName("avatar_subscript_url")
145 | val avatarSubscriptUrl: String,
146 | @SerializedName("due_date")
147 | val dueDate: Long,
148 | @SerializedName("label")
149 | val label: Label,
150 | @SerializedName("nickname_color")
151 | val nicknameColor: String,
152 | @SerializedName("role")
153 | val role: Int,
154 | @SerializedName("status")
155 | val status: Int,
156 | @SerializedName("theme_type")
157 | val themeType: Int,
158 | @SerializedName("type")
159 | val type: Int,
160 | @SerializedName("vip_pay_type")
161 | val vipPayType: Int
162 | ) {
163 | data class Label(
164 | @SerializedName("bg_color")
165 | val bgColor: String,
166 | @SerializedName("bg_style")
167 | val bgStyle: Int,
168 | @SerializedName("border_color")
169 | val borderColor: String,
170 | @SerializedName("label_theme")
171 | val labelTheme: String,
172 | @SerializedName("path")
173 | val path: String,
174 | @SerializedName("text")
175 | val text: String,
176 | @SerializedName("text_color")
177 | val textColor: String
178 | )
179 | }
180 | }
181 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/bilibili/BilibiliUtil.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.bilibili
2 |
3 | import com.google.gson.Gson
4 | import kotlinx.coroutines.delay
5 | import me.rerere.zhiwang.util.net.await
6 | import okhttp3.OkHttpClient
7 | import okhttp3.Request
8 | import org.jsoup.Jsoup
9 |
10 | class BilibiliUtil(
11 | private val okHttpClient: OkHttpClient
12 | ) {
13 | private val gson = Gson()
14 |
15 | suspend fun getUserInfo(mId: Int): BilibiliUserData? = try {
16 | val request = Request.Builder()
17 | .url("https://api.bilibili.com/x/space/acc/info?mid=$mId")
18 | .get()
19 | .build()
20 | val response = okHttpClient.newCall(request).await()
21 | require(response.isSuccessful)
22 | val json = response.body?.string()
23 | // println(json)
24 | val user = gson.fromJson(json, BilibiliUserData::class.java)
25 | user
26 | } catch (e: Exception) {
27 | e.printStackTrace()
28 | null
29 | }
30 |
31 | suspend fun getAllSubLis(profileLink: String): SubResult? = try {
32 | val profileRequest = Request.Builder()
33 | .url(profileLink)
34 | .get()
35 | .build()
36 |
37 | val profileResponse = okHttpClient.newCall(profileRequest).await()
38 | // 用户ID
39 | val id = profileResponse.request.url.toString().let {
40 | it.substring(
41 | it.indexOf("com/") + 4,
42 | it.indexOf('?', it.indexOf("com/") + 4)
43 | )
44 | }
45 | // 用户名
46 | val name = Jsoup.parse(profileResponse.body?.string()).title().let {
47 | it.substring(0 until it.indexOf("的个人空间"))
48 | }
49 | // 关注的用户
50 | val list = hashSetOf()
51 | repeat(5) {
52 | val subRequest = Request.Builder()
53 | .url("https://api.bilibili.com/x/relation/followings?vmid=$id&pn=${it + 1}&ps=50&order=desc")
54 | .get()
55 | .build()
56 | val subList = try {
57 | val subResponse = okHttpClient.newCall(subRequest).await()
58 | gson.fromJson(subResponse.body!!.string(), SubList::class.java)
59 | } catch (e: Exception) {
60 | e.printStackTrace()
61 | null
62 | }
63 | subList?.data?.list?.forEach { user ->
64 | list.add(
65 | SubUser(mid = user.mid, name = user.uname)
66 | )
67 | }
68 | delay(50)
69 | }
70 | repeat(5) {
71 | val subRequest = Request.Builder()
72 | .url("https://api.bilibili.com/x/relation/followings?vmid=$id&pn=${it + 1}&ps=50&order=asc")
73 | .get()
74 | .build()
75 | val subList = try {
76 | val subResponse = okHttpClient.newCall(subRequest).await()
77 | gson.fromJson(subResponse.body!!.string(), SubList::class.java)
78 | } catch (e: Exception) {
79 | e.printStackTrace()
80 | null
81 | }
82 | subList?.data?.list?.forEach { user ->
83 | list.add(
84 | SubUser(mid = user.mid, name = user.uname)
85 | )
86 | }
87 | delay(50)
88 | }
89 | list.forEach {
90 | println("${it.mid} - ${it.name}")
91 | }
92 | val result = SubResult(
93 | id = id,
94 | name = name,
95 | subList = list
96 | )
97 | result
98 | } catch (e: Exception) {
99 | e.printStackTrace()
100 | null
101 | }
102 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/bilibili/SubList.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.bilibili
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class SubList(
7 | @SerializedName("code")
8 | val code: Int,
9 | @SerializedName("data")
10 | val `data`: Data,
11 | @SerializedName("message")
12 | val message: String,
13 | @SerializedName("ttl")
14 | val ttl: Int
15 | ) {
16 | data class Data(
17 | @SerializedName("list")
18 | val list: List,
19 | @SerializedName("re_version")
20 | val reVersion: Long,
21 | @SerializedName("total")
22 | val total: Int
23 | ) {
24 | data class User(
25 | @SerializedName("attribute")
26 | val attribute: Int,
27 | @SerializedName("face")
28 | val face: String,
29 | @SerializedName("mid")
30 | val mid: Int,
31 | @SerializedName("mtime")
32 | val mtime: Int,
33 | @SerializedName("sign")
34 | val sign: String,
35 | @SerializedName("special")
36 | val special: Int,
37 | @SerializedName("tag")
38 | val tag: Any,
39 | @SerializedName("uname")
40 | val uname: String,
41 | )
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/bilibili/SubResult.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.bilibili
2 |
3 | data class SubResult(
4 | val id: String,
5 | val name: String,
6 | val subList: Set
7 | )
8 |
9 | data class SubUser(
10 | val mid: Int,
11 | val name: String
12 | )
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/wiki/WikiList.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.wiki
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | class WikiList : ArrayList(){
7 | data class WikiListItem(
8 | @SerializedName("description")
9 | val description: String,
10 | @SerializedName("title")
11 | val title: String
12 | )
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/wiki/WikiUtil.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.wiki
2 |
3 | import com.google.gson.Gson
4 | import me.rerere.zhiwang.util.net.await
5 | import okhttp3.OkHttpClient
6 | import okhttp3.Request
7 | import javax.inject.Inject
8 |
9 | class WikiUtil @Inject constructor(
10 | private val okHttpClient: OkHttpClient
11 | ){
12 | private val gson = Gson()
13 |
14 | suspend fun loadWiki() : List? {
15 | return try {
16 | val request = Request.Builder()
17 | .url("https://cdn.jsdelivr.net/gh/jiangdashao/ASoulZhiWang/wiki.json")
18 | .get()
19 | .build()
20 | val response = okHttpClient.newCall(request).await()
21 | val body = response.body?.string()?.trim()
22 | val wikiList = gson.fromJson(body, WikiList::class.java)
23 | wikiList
24 | }catch (e: Exception){
25 | e.printStackTrace()
26 | null
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/zhiwang/Request.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.zhiwang
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class Request(
6 | @SerializedName("text")
7 | val text: String
8 | )
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/zhiwang/Response.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.zhiwang
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class Response(
7 | @SerializedName("code")
8 | val code: Int,
9 | @SerializedName("data")
10 | val `data`: Data,
11 | @SerializedName("message")
12 | val message: String
13 | ) {
14 | data class Data(
15 | @SerializedName("end_time")
16 | val endTime: Int,
17 | @SerializedName("rate")
18 | val rate: Double,
19 | @SerializedName("related")
20 | val related: List,
21 | @SerializedName("start_time")
22 | val startTime: Int
23 | ) {
24 | data class Related(
25 | @SerializedName("rate")
26 | val rate: Double,
27 | @SerializedName("reply")
28 | val reply: Reply,
29 | @SerializedName("reply_url")
30 | val replyUrl: String
31 | ) {
32 | data class Reply(
33 | @SerializedName("content")
34 | val content: String,
35 | @SerializedName("ctime")
36 | val ctime: Int,
37 | @SerializedName("dynamic_id")
38 | val dynamicId: String,
39 | @SerializedName("like_num")
40 | val likeNum: Int,
41 | @SerializedName("m_name")
42 | val mName: String,
43 | @SerializedName("mid")
44 | val mid: Int,
45 | @SerializedName("oid")
46 | val oid: String,
47 | @SerializedName("origin_rpid")
48 | val originRpid: String,
49 | @SerializedName("rpid")
50 | val rpid: String,
51 | @SerializedName("similar_count")
52 | val similarCount: Int,
53 | @SerializedName("similar_like_sum")
54 | val similarLikeSum: Int,
55 | @SerializedName("type_id")
56 | val typeId: Int,
57 |
58 | var avatar: String = ""
59 | )
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/zhiwang/ZhiWangService.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.zhiwang
2 |
3 | import retrofit2.http.Body
4 | import retrofit2.http.Headers
5 | import retrofit2.http.POST
6 |
7 | interface ZhiWangService {
8 | @POST("/v1/api/check")
9 | @Headers("Content-Type: application/json")
10 | suspend fun query(@Body content: Request) : Response
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/zuowen/ZuowenContent.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.zuowen
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class ZuowenContent(
7 | @SerializedName("htmlContent")
8 | val htmlContent: String
9 | )
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/zuowen/ZuowenPageSource.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.zuowen
2 |
3 | import android.util.Log
4 | import androidx.paging.PagingSource
5 | import androidx.paging.PagingState
6 | import me.rerere.zhiwang.repo.ZuowenRepo
7 |
8 | private const val TAG = "ZuowenPageSource"
9 |
10 | class ZuowenPageSource(
11 | private val zuowenRepo: ZuowenRepo
12 | ) : PagingSource() {
13 | override fun getRefreshKey(state: PagingState): Int {
14 | return 0
15 | }
16 |
17 | override suspend fun load(params: LoadParams): LoadResult {
18 | val page = params.key ?: 0
19 | Log.i(TAG, "loading page: $page, ${params.loadSize}")
20 | val data = zuowenRepo.getZuowen(pageNum = page, pageSize = params.loadSize)
21 | return if (data != null) {
22 | LoadResult.Page(
23 | prevKey = if (page <= 0) null else page - 1,
24 | nextKey = if(data.articles.size < params.loadSize) null else page + 1,
25 | data = data.articles
26 | )
27 | } else {
28 | LoadResult.Error(
29 | Exception("Failed to load zuowen list")
30 | )
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/zuowen/ZuowenResponse.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.zuowen
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class ZuowenResponse(
7 | @SerializedName("articles")
8 | val articles: List,
9 | @SerializedName("count")
10 | val count: Int,
11 | @SerializedName("info")
12 | val info: String
13 | ) {
14 | data class Article(
15 | @SerializedName("author")
16 | val author: String,
17 | @SerializedName("_id")
18 | val id: String,
19 | @SerializedName("plainContent")
20 | val plainContent: String,
21 | @SerializedName("submissionTime")
22 | val submissionTime: Int,
23 | @SerializedName("tags")
24 | val tags: List,
25 | @SerializedName("title")
26 | val title: String
27 | )
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/api/zuowen/ZuowenService.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.api.zuowen
2 |
3 | import retrofit2.http.GET
4 | import retrofit2.http.Path
5 | import retrofit2.http.Query
6 |
7 | interface ZuowenService {
8 | @GET("/v/articles")
9 | suspend fun getZuowenList(@Query("pageNum") page: Int, @Query("pageSize") pageSize: Int) : ZuowenResponse
10 |
11 | @GET("/v/articles/{id}/html")
12 | suspend fun getZuowenContent(@Path("id") id: String) : ZuowenContent
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.di
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import me.rerere.zhiwang.api.bilibili.BilibiliUtil
8 | import me.rerere.zhiwang.api.zhiwang.ZhiWangService
9 | import me.rerere.zhiwang.api.zuowen.ZuowenService
10 | import me.rerere.zhiwang.repo.ZuowenRepo
11 | import me.rerere.zhiwang.util.net.UserAgentInterceptor
12 | import okhttp3.OkHttpClient
13 | import retrofit2.Retrofit
14 | import retrofit2.converter.gson.GsonConverterFactory
15 | import java.util.concurrent.TimeUnit
16 | import javax.inject.Qualifier
17 | import javax.inject.Singleton
18 |
19 | // User Agent
20 | private const val USER_AGENT =
21 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"
22 |
23 | @Module
24 | @InstallIn(SingletonComponent::class)
25 | object NetworkModule {
26 | private const val TIMEOUT = 3000L
27 |
28 | @Provides
29 | @Singleton
30 | fun provideHttpClient(): OkHttpClient = OkHttpClient.Builder()
31 | .connectTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
32 | .readTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
33 | .callTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
34 | .addInterceptor(UserAgentInterceptor(USER_AGENT))
35 | .build()
36 |
37 | @Provides
38 | @Singleton
39 | fun provideBilibiliUtil(okHttpClient: OkHttpClient) = BilibiliUtil(okHttpClient)
40 |
41 | @ZhiwangRetrofit
42 | @Provides
43 | @Singleton
44 | fun provideZhiwangRetrofitClient(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder()
45 | .client(okHttpClient)
46 | .baseUrl("https://asoulcnki.asia")
47 | .addConverterFactory(GsonConverterFactory.create())
48 | .build()
49 |
50 | @Provides
51 | @Singleton
52 | fun provideZhiwangService(@ZhiwangRetrofit retrofit: Retrofit): ZhiWangService = retrofit
53 | .create(ZhiWangService::class.java)
54 |
55 | @ZuowenRetrofit
56 | @Provides
57 | @Singleton
58 | fun provideZuowenRetrofit(okHttpClient: OkHttpClient) = Retrofit.Builder()
59 | .client(okHttpClient)
60 | .baseUrl("https://asoul.icu")
61 | .addConverterFactory(GsonConverterFactory.create())
62 | .build()
63 |
64 | @Provides
65 | @Singleton
66 | fun provideZuowenService(@ZuowenRetrofit retrofit: Retrofit) : ZuowenService = retrofit
67 | .create(ZuowenService::class.java)
68 |
69 | @Provides
70 | @Singleton
71 | fun provideZhiwangRepo(zhiWangService: ZhiWangService, zuowenService: ZuowenService, bilibiliUtil: BilibiliUtil) = ZuowenRepo(zhiWangService, zuowenService, bilibiliUtil)
72 | }
73 |
74 | @Qualifier
75 | annotation class ZhiwangRetrofit
76 |
77 | @Qualifier
78 | annotation class ZuowenRetrofit
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/repo/WikiRepo.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.repo
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.withContext
5 | import me.rerere.zhiwang.api.wiki.WikiUtil
6 | import javax.inject.Inject
7 |
8 | class WikiRepo @Inject constructor(
9 | private val wikiUtil: WikiUtil
10 | ) {
11 | suspend fun loadWiki() = withContext(Dispatchers.IO) { wikiUtil.loadWiki() }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/repo/ZuowenRepo.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.repo
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.withContext
5 | import me.rerere.zhiwang.api.bilibili.BilibiliUtil
6 | import me.rerere.zhiwang.api.bilibili.SubResult
7 | import me.rerere.zhiwang.api.zhiwang.Request
8 | import me.rerere.zhiwang.api.zhiwang.ZhiWangService
9 | import me.rerere.zhiwang.api.zuowen.ZuowenService
10 | import me.rerere.zhiwang.util.net.autoRetry
11 |
12 | class ZuowenRepo(
13 | private val zhiWangService: ZhiWangService,
14 | private val zuowenService: ZuowenService,
15 | private val bilibiliUtil: BilibiliUtil
16 | ) {
17 | suspend fun query(content: Request) = withContext(Dispatchers.IO) {
18 | autoRetry {
19 | zhiWangService.query(content)
20 | }?.apply {
21 | println(this)
22 |
23 | // 获取用户信息
24 | this.data.related.forEach {
25 | try {
26 | val mid = it.reply.mid
27 | val bilibiliUser = bilibiliUtil.getUserInfo(mid)
28 | bilibiliUser?.let { user ->
29 | it.reply.avatar = user.data.face.replace("http:", "https:")
30 | println("头像: ${user.data.face}")
31 | }
32 | }catch (e: Exception){
33 | e.printStackTrace()
34 | }
35 | }
36 | }
37 | }
38 |
39 | suspend fun getZuowen(pageNum: Int, pageSize: Int) = withContext(Dispatchers.IO) {
40 | try {
41 | zuowenService.getZuowenList(pageNum, pageSize)
42 | } catch (e: Exception){
43 | e.printStackTrace()
44 | null
45 | }
46 | }
47 |
48 | suspend fun getZuowenContent(id: String) = withContext(Dispatchers.IO){
49 | try {
50 | zuowenService.getZuowenContent(id)
51 | } catch (e: Exception){
52 | e.printStackTrace()
53 | null
54 | }
55 | }
56 |
57 | suspend fun getAllSubLis(link: String) : SubResult? = withContext(Dispatchers.IO){
58 | bilibiliUtil.getAllSubLis(link)
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/ui/public/XiaoZuoWen.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.ui.public
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.widget.Toast
6 | import androidx.compose.foundation.Image
7 | import androidx.compose.foundation.gestures.detectTapGestures
8 | import androidx.compose.foundation.layout.*
9 | import androidx.compose.foundation.shape.CircleShape
10 | import androidx.compose.foundation.shape.RoundedCornerShape
11 | import androidx.compose.material.*
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.filled.ThumbUp
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.CompositionLocalProvider
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.draw.clip
19 | import androidx.compose.ui.input.pointer.pointerInput
20 | import androidx.compose.ui.platform.LocalContext
21 | import androidx.compose.ui.text.font.FontWeight
22 | import androidx.compose.ui.unit.dp
23 | import androidx.constraintlayout.compose.ConstraintLayout
24 | import coil.compose.ImagePainter
25 | import coil.compose.rememberImagePainter
26 | import com.google.accompanist.placeholder.material.placeholder
27 | import me.rerere.zhiwang.api.zhiwang.Response
28 | import me.rerere.zhiwang.ui.theme.PINK
29 | import me.rerere.zhiwang.util.android.setClipboardText
30 | import java.text.SimpleDateFormat
31 | import java.util.*
32 |
33 | @Composable
34 | fun XiaoZuoWen(data: Response.Data.Related) {
35 | val context = LocalContext.current
36 | Card(
37 | modifier = Modifier
38 | .fillMaxWidth()
39 | .padding(8.dp)
40 | .pointerInput(Unit) {
41 | detectTapGestures(
42 | onLongPress = {
43 | context.setClipboardText(data.reply.content)
44 | Toast
45 | .makeText(context, "已复制该作文到剪贴板", Toast.LENGTH_SHORT)
46 | .show()
47 | },
48 | onTap = {
49 | val intent = Intent(
50 | Intent.ACTION_VIEW,
51 | Uri.parse(data.replyUrl.trim())
52 | )
53 | context.startActivity(intent)
54 | }
55 | )
56 | },
57 | shape = RoundedCornerShape(4.dp),
58 | elevation = 4.dp
59 | ) {
60 | Column(Modifier.padding(12.dp)) {
61 | // 作者信息
62 | ConstraintLayout(
63 | modifier = Modifier
64 | .fillMaxWidth()
65 | .padding(vertical = 4.dp)
66 | ) {
67 | val (avatar, name, likes) = createRefs()
68 | val painter = rememberImagePainter(data.reply.avatar as? String)
69 | Box(modifier = Modifier
70 | .constrainAs(avatar) {
71 | start.linkTo(parent.start, 8.dp)
72 | top.linkTo(parent.top)
73 | }
74 | .size(40.dp)
75 | .clip(CircleShape)
76 | .placeholder(painter.state is ImagePainter.State.Loading)
77 | ) {
78 | Image(
79 | painter = painter,
80 | contentDescription = null,
81 | modifier = Modifier.fillMaxSize()
82 | )
83 | }
84 | // 名字
85 | Text(
86 | modifier = Modifier.constrainAs(name) {
87 | start.linkTo(avatar.end, 8.dp)
88 | centerVerticallyTo(avatar)
89 | },
90 | text = data.reply.mName,
91 | fontWeight = FontWeight.Bold,
92 | color = PINK
93 | )
94 | // 点赞
95 | Row(modifier = Modifier.constrainAs(likes) {
96 | end.linkTo(parent.end, 8.dp)
97 | centerVerticallyTo(avatar)
98 | }, verticalAlignment = Alignment.CenterVertically) {
99 | Icon(
100 | modifier = Modifier.size(15.dp),
101 | imageVector = Icons.Default.ThumbUp,
102 | contentDescription = null
103 | )
104 | Spacer(modifier = Modifier.width(3.dp))
105 | Text(text = (data.reply.likeNum.toString()))
106 | }
107 | }
108 | // 作文内容
109 | CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
110 | Text(text = data.reply.content)
111 | }
112 | // 作文信息
113 | CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
114 | Row(
115 | modifier = Modifier.fillMaxWidth(),
116 | verticalAlignment = Alignment.CenterVertically
117 | ) {
118 | Text(text = "重复度: ${(data.rate * 100f).toInt().coerceAtMost(100)}%")
119 | Spacer(modifier = Modifier.width(8.dp))
120 | Text(text = "日期: ${getDateTime((data.reply.ctime.toLong() * 1000L))}")
121 | }
122 | }
123 | }
124 | }
125 | }
126 |
127 | private val timeFormat = SimpleDateFormat("yyyy/MM/dd HH:mm", Locale.CHINA)
128 | fun getDateTime(time: Long): String? {
129 | return try {
130 | return timeFormat.format(Date(time))
131 | } catch (e: Exception) {
132 | e.toString()
133 | }
134 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/ui/screen/index/IndexScreen.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.ui.screen.index
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.compose.animation.ExperimentalAnimationApi
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material.*
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.Explore
12 | import androidx.compose.material.icons.filled.FindInPage
13 | import androidx.compose.material.icons.filled.Info
14 | import androidx.compose.material.icons.filled.Public
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.rememberCoroutineScope
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.platform.LocalContext
19 | import androidx.compose.ui.unit.dp
20 | import androidx.hilt.navigation.compose.hiltViewModel
21 | import androidx.navigation.NavController
22 | import com.google.accompanist.insets.LocalWindowInsets
23 | import com.google.accompanist.insets.navigationBarsPadding
24 | import com.google.accompanist.insets.rememberInsetsPaddingValues
25 | import com.google.accompanist.pager.ExperimentalPagerApi
26 | import com.google.accompanist.pager.HorizontalPager
27 | import com.google.accompanist.pager.rememberPagerState
28 | import kotlinx.coroutines.launch
29 | import me.rerere.zhiwang.ui.screen.index.page.AboutPage
30 | import me.rerere.zhiwang.ui.screen.index.page.Content
31 | import me.rerere.zhiwang.ui.screen.index.page.WikiPage
32 | import me.rerere.zhiwang.ui.screen.index.page.Zuowen
33 | import me.rerere.zhiwang.ui.theme.uiBackGroundColor
34 | import me.rerere.zhiwang.util.noRippleClickable
35 |
36 | val pages = mapOf(
37 | 0 to "小作文查重",
38 | 1 to "小作文库",
39 | 2 to "百科",
40 | 3 to "关于"
41 | )
42 |
43 | @ExperimentalPagerApi
44 | @ExperimentalAnimationApi
45 | @Composable
46 | fun IndexScreen(
47 | navController: NavController,
48 | indexScreenVideoModel: IndexScreenVideoModel = hiltViewModel()
49 | ) {
50 | val scaffoldState = rememberScaffoldState()
51 | val pager = rememberPagerState()
52 | val coroutineScope = rememberCoroutineScope()
53 | Scaffold(
54 | scaffoldState = scaffoldState,
55 | topBar = {
56 | TopBar(indexScreenVideoModel, pages[pager.currentPage] ?: "")
57 | },
58 | bottomBar = {
59 | BottomNavigation(modifier = Modifier.navigationBarsPadding(), backgroundColor = MaterialTheme.colors.uiBackGroundColor) {
60 | BottomNavigationItem(
61 | selected = pager.currentPage == 0,
62 | onClick = { coroutineScope.launch { pager.animateScrollToPage(0) } },
63 | icon = {
64 | Icon(Icons.Default.FindInPage, null)
65 | }, label = {
66 | Text(text = pages[0] ?: "")
67 | })
68 | BottomNavigationItem(
69 | selected = pager.currentPage == 1,
70 | onClick = { coroutineScope.launch { pager.animateScrollToPage(1) } },
71 | icon = {
72 | Icon(Icons.Default.Public, null)
73 | }, label = {
74 | Text(text = pages[1] ?: "")
75 | })
76 | BottomNavigationItem(
77 | selected = pager.currentPage == 2,
78 | onClick = { coroutineScope.launch { pager.animateScrollToPage(2) } },
79 | icon = {
80 | Icon(Icons.Default.Explore, null)
81 | }, label = {
82 | Text(text = pages[2] ?: "")
83 | })
84 | BottomNavigationItem(
85 | selected = pager.currentPage == 3,
86 | onClick = { coroutineScope.launch { pager.animateScrollToPage(3) } },
87 | icon = {
88 | Icon(Icons.Default.Info, null)
89 | }, label = {
90 | Text(text = pages[3] ?: "")
91 | })
92 | }
93 | }
94 | ) {
95 | Box(modifier = Modifier.padding(it)) {
96 | HorizontalPager(modifier = Modifier.fillMaxSize(), state = pager, count = 4) {
97 | when (it) {
98 | 0 -> {
99 | Box(modifier = Modifier.fillMaxSize()) {
100 | Content(
101 | indexScreenVideoModel = indexScreenVideoModel,
102 | scaffoldState = scaffoldState
103 | )
104 | }
105 | }
106 | 1 -> {
107 | Box(modifier = Modifier.fillMaxSize()) {
108 | Zuowen(
109 | indexScreenVideoModel = indexScreenVideoModel,
110 | navController = navController
111 | )
112 | }
113 | }
114 | 2 -> {
115 | Box(modifier = Modifier.fillMaxSize()) {
116 | WikiPage(
117 | navController = navController,
118 | indexViewModel = indexScreenVideoModel
119 | )
120 | }
121 | }
122 | 3 -> {
123 | Box(modifier = Modifier.fillMaxSize()) {
124 | AboutPage()
125 | }
126 | }
127 | }
128 | }
129 | }
130 | }
131 | }
132 |
133 |
134 | @Composable
135 | private fun TopBar(indexScreenVideoModel: IndexScreenVideoModel, title: String) {
136 | val context = LocalContext.current
137 | com.google.accompanist.insets.ui.TopAppBar(
138 | title = {
139 | Text(text = title)
140 | },
141 | actions = {
142 | if (indexScreenVideoModel.foundUpdate) {
143 | Text(text = "APP有更新", modifier = Modifier
144 | .noRippleClickable {
145 | val intent = Intent(
146 | Intent.ACTION_VIEW,
147 | Uri.parse("https://github.com/jiangdashao/ASoulZhiWang/releases/latest")
148 | )
149 | context.startActivity(intent)
150 | }
151 | .padding(horizontal = 16.dp))
152 | }
153 | },
154 | contentPadding = rememberInsetsPaddingValues(insets = LocalWindowInsets.current.statusBars, applyBottom = false)
155 | )
156 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/ui/screen/index/IndexScreenVideoModel.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.ui.screen.index
2 |
3 | import android.util.Log
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.MutableLiveData
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.viewModelScope
10 | import androidx.paging.Pager
11 | import androidx.paging.PagingConfig
12 | import androidx.paging.cachedIn
13 | import dagger.hilt.android.lifecycle.HiltViewModel
14 | import kotlinx.coroutines.launch
15 | import me.rerere.zhiwang.AppContext
16 | import me.rerere.zhiwang.api.bilibili.SubUser
17 | import me.rerere.zhiwang.api.wiki.WikiList
18 | import me.rerere.zhiwang.api.zhiwang.Request
19 | import me.rerere.zhiwang.api.zhiwang.Response
20 | import me.rerere.zhiwang.api.zuowen.ZuowenPageSource
21 | import me.rerere.zhiwang.repo.WikiRepo
22 | import me.rerere.zhiwang.repo.ZuowenRepo
23 | import me.rerere.zhiwang.util.checkUpdate
24 | import javax.inject.Inject
25 |
26 | @HiltViewModel
27 | class IndexScreenVideoModel @Inject constructor(
28 | private val zhiwangRepo: ZuowenRepo,
29 | private val wikiRepo: WikiRepo
30 | ) : ViewModel() {
31 | // 查重
32 | var loading by mutableStateOf(false)
33 | var content by mutableStateOf("")
34 | val queryResult = MutableLiveData()
35 | var error by mutableStateOf(false)
36 | var lastQuery by mutableStateOf(0L)
37 |
38 |
39 | // 找到更新
40 | var foundUpdate by mutableStateOf(false)
41 |
42 | init {
43 | // 检查更新
44 | viewModelScope.launch {
45 | foundUpdate = checkUpdate(AppContext.appContext)
46 | }
47 | }
48 |
49 | // 小作文
50 | val pager = Pager(
51 | config = PagingConfig(
52 | pageSize = 32,
53 | prefetchDistance = 1,
54 | initialLoadSize = 32
55 | )
56 | ) {
57 | ZuowenPageSource(zhiwangRepo)
58 | }.flow.cachedIn(viewModelScope)
59 |
60 | // 查成分
61 | var profileLink by mutableStateOf("")
62 | var cfLoading by mutableStateOf(false)
63 | var cfError by mutableStateOf(false)
64 | var name by mutableStateOf("")
65 | var sublist by mutableStateOf(emptySet())
66 |
67 | // WIKI
68 | var wikiList by mutableStateOf(emptyList())
69 | var wikiLoading by mutableStateOf(false)
70 | var wikiError by mutableStateOf(false)
71 |
72 | init {
73 | loadWiki()
74 | }
75 |
76 | fun loadWiki() {
77 | viewModelScope.launch {
78 | wikiLoading = true
79 | wikiError = false
80 |
81 | val result = wikiRepo.loadWiki()
82 | result?.let {
83 | wikiList = it
84 | println("Loaded ${wikiList.size} wiki items")
85 | } ?: kotlin.run {
86 | wikiError = true
87 | }
88 |
89 | wikiLoading = false
90 | }
91 | }
92 |
93 | fun chaChengFen(){
94 | viewModelScope.launch {
95 | cfLoading = true
96 | cfError = false
97 |
98 | val result = zhiwangRepo.getAllSubLis(profileLink)
99 | result?.let {
100 | name = it.name
101 | sublist = it.subList
102 | } ?: kotlin.run {
103 | cfError = true
104 | }
105 |
106 | cfLoading = false
107 | }
108 | }
109 |
110 | fun query() {
111 | lastQuery = System.currentTimeMillis()
112 | viewModelScope.launch {
113 | loading = true
114 | error = false
115 |
116 | // 查重
117 | val result = zhiwangRepo.query(Request(content))
118 | queryResult.value = result
119 |
120 | loading = false
121 | if (queryResult.value == null) error = true
122 | }
123 | }
124 |
125 | fun resetResult() {
126 | queryResult.value = null
127 | }
128 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/ui/screen/index/page/About.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.ui.screen.index.page
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.foundation.rememberScrollState
6 | import androidx.compose.foundation.shape.RoundedCornerShape
7 | import androidx.compose.foundation.verticalScroll
8 | import androidx.compose.material.Card
9 | import androidx.compose.material.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.draw.clip
13 | import androidx.compose.ui.layout.ContentScale
14 | import androidx.compose.ui.res.painterResource
15 | import androidx.compose.ui.text.font.FontWeight
16 | import androidx.compose.ui.unit.dp
17 | import androidx.compose.ui.unit.sp
18 | import me.rerere.zhiwang.R
19 |
20 | @Composable
21 | fun AboutPage() {
22 | Column(
23 | modifier = Modifier
24 | .padding(16.dp)
25 | .verticalScroll(state = rememberScrollState())
26 | ) {
27 | Card {
28 | Column(Modifier.padding(16.dp)) {
29 | Image(
30 | modifier = Modifier
31 | .fillMaxWidth()
32 | .height(250.dp)
33 | .clip(RoundedCornerShape(4.dp)),
34 | painter = painterResource(R.drawable.asoul),
35 | contentDescription = null,
36 | contentScale = ContentScale.FillWidth
37 | )
38 | Text(text = "ASoul简介", fontWeight = FontWeight.Bold, fontSize = 20.sp)
39 | Text(text = "A-SOUL是乐华娱乐年度最新企划中打造的虚拟偶像女团,成员由向晚(Ava)、贝拉(Bella)、珈乐(Carol)、嘉然(Diana)、乃琳(Eileen)五人组成,于2020年11月以“乐华娱乐首个虚拟偶像团体”名义出道。")
40 | }
41 | }
42 | Spacer(modifier = Modifier.height(30.dp))
43 | Card {
44 | Column(Modifier.padding(16.dp)) {
45 | Text(text = "本APP简介", fontWeight = FontWeight.Bold, fontSize = 20.sp)
46 | Text(text = "项目地址: https://github.com/jiangdashao/asoulzhiwang")
47 | Text(text = "非常感谢:", fontWeight = FontWeight.Bold, fontSize = 20.sp)
48 | Text(text = "查重API: https://asoulcnki.asia/")
49 | Text(text = "小作文库: https://asoul.icu/")
50 | }
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/ui/screen/index/page/Chachong.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.ui.screen.index.page
2 |
3 | import android.content.ClipData
4 | import android.content.ClipboardManager
5 | import android.content.Context
6 | import android.widget.Toast
7 | import androidx.activity.compose.BackHandler
8 | import androidx.compose.animation.ExperimentalAnimationApi
9 | import androidx.compose.animation.animateContentSize
10 | import androidx.compose.foundation.background
11 | import androidx.compose.foundation.layout.*
12 | import androidx.compose.foundation.rememberScrollState
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.foundation.verticalScroll
15 | import androidx.compose.material.*
16 | import androidx.compose.material.icons.Icons
17 | import androidx.compose.material.icons.filled.Clear
18 | import androidx.compose.material.icons.filled.ContentPaste
19 | import androidx.compose.runtime.*
20 | import androidx.compose.runtime.livedata.observeAsState
21 | import androidx.compose.ui.Alignment
22 | import androidx.compose.ui.Modifier
23 | import androidx.compose.ui.graphics.Color
24 | import androidx.compose.ui.platform.LocalContext
25 | import androidx.compose.ui.platform.LocalFocusManager
26 | import androidx.compose.ui.text.font.FontWeight
27 | import androidx.compose.ui.text.style.TextAlign
28 | import androidx.compose.ui.unit.dp
29 | import androidx.compose.ui.unit.sp
30 | import com.google.accompanist.placeholder.PlaceholderHighlight
31 | import com.google.accompanist.placeholder.material.placeholder
32 | import com.google.accompanist.placeholder.material.shimmer
33 | import me.rerere.zhiwang.ui.public.XiaoZuoWen
34 | import me.rerere.zhiwang.ui.public.getDateTime
35 | import me.rerere.zhiwang.ui.screen.index.IndexScreenVideoModel
36 | import me.rerere.zhiwang.util.android.getClipboardContent
37 | import me.rerere.zhiwang.util.format.format
38 | import me.rerere.zhiwang.util.noRippleClickable
39 |
40 | @ExperimentalAnimationApi
41 | @Composable
42 | fun Content(indexScreenVideoModel: IndexScreenVideoModel, scaffoldState: ScaffoldState) {
43 | // val coroutineScope = rememberCoroutineScope()
44 | val response by indexScreenVideoModel.queryResult.observeAsState()
45 | var error by remember {
46 | mutableStateOf(false)
47 | }
48 | val context = LocalContext.current
49 |
50 | // 返回处理
51 | // 显示了查重结果的时候点击返回键会清空查重结果,方便重新查重
52 | BackHandler(response != null) {
53 | indexScreenVideoModel.resetResult()
54 | }
55 |
56 | // 查重页面
57 | Column(
58 | modifier = Modifier
59 | .fillMaxWidth()
60 | .verticalScroll(rememberScrollState())
61 | ) {
62 | // 输入框
63 | Box(contentAlignment = Alignment.BottomEnd) {
64 | TextField(
65 | modifier = Modifier
66 | .fillMaxWidth()
67 | .animateContentSize()
68 | .let {
69 | if (response == null) {
70 | it.height(200.dp)
71 | } else {
72 | it.wrapContentHeight()
73 | }
74 | }
75 | .padding(16.dp),
76 | value = indexScreenVideoModel.content,
77 | onValueChange = {
78 | if (it.length >= 10) {
79 | error = false
80 | }
81 | indexScreenVideoModel.content = it
82 | },
83 | label = {
84 | Text(text = "输入要查重的小作文, 至少10个字哦")
85 | },
86 | colors = TextFieldDefaults.textFieldColors(
87 | focusedIndicatorColor = Color.Transparent,
88 | unfocusedIndicatorColor = Color.Transparent
89 | ),
90 | shape = RoundedCornerShape(5.dp),
91 | isError = error,
92 | maxLines = if (response == null) 8 else 1
93 | )
94 |
95 | // 输入框上的按钮
96 | Row(
97 | modifier = Modifier
98 | .padding(20.dp),
99 | verticalAlignment = Alignment.CenterVertically
100 | ) {
101 | // 从剪贴板粘贴
102 | androidx.compose.animation.AnimatedVisibility(visible = indexScreenVideoModel.queryResult.value == null) {
103 | Icon(modifier = Modifier.noRippleClickable {
104 | val text = context.getClipboardContent()
105 | text?.let {
106 | indexScreenVideoModel.content = it
107 | } ?: kotlin.run {
108 | Toast.makeText(context, "剪贴板没有内容", Toast.LENGTH_SHORT).show()
109 | }
110 | }, imageVector = Icons.Default.ContentPaste, contentDescription = null)
111 | }
112 |
113 | Spacer(modifier = Modifier.width(4.dp))
114 |
115 | // 清空
116 | androidx.compose.animation.AnimatedVisibility(visible = indexScreenVideoModel.content.isNotEmpty()) {
117 | Icon(modifier = Modifier.noRippleClickable {
118 | indexScreenVideoModel.content = ""
119 | indexScreenVideoModel.queryResult.value = null
120 | }, imageVector = Icons.Default.Clear, contentDescription = null)
121 | }
122 | }
123 | }
124 | val focusManager = LocalFocusManager.current
125 |
126 | // 查重按钮
127 | Button(
128 | modifier = Modifier
129 | .fillMaxWidth()
130 | .padding(horizontal = 16.dp, vertical = 4.dp),
131 | onClick = {
132 | focusManager.clearFocus()
133 |
134 | if (indexScreenVideoModel.content.length < 10) {
135 | // 小作文长度不够
136 | error = true
137 | Toast.makeText(context, "小作文至少需要10个字哦", Toast.LENGTH_SHORT).show()
138 | } else if (System.currentTimeMillis() - indexScreenVideoModel.lastQuery <= 5000L) {
139 | Toast.makeText(context, "请等待 5 秒再查重哦!", Toast.LENGTH_SHORT).show()
140 | } else {
141 | // 开始查询
142 | indexScreenVideoModel.resetResult()
143 | indexScreenVideoModel.query()
144 | }
145 | }) {
146 | Text(text = "立即查重捏 🤤")
147 | }
148 |
149 | // 加载动画
150 | if (indexScreenVideoModel.loading) {
151 | val width = listOf(0.9f, 1f, 0.87f, 0.83f, 0.89f, 0.86f)
152 | repeat(6) {
153 | Spacer(
154 | modifier = Modifier
155 | .fillMaxWidth(width[it])
156 | .height(90.dp)
157 | .padding(16.dp)
158 | .placeholder(visible = true, highlight = PlaceholderHighlight.shimmer())
159 | )
160 | }
161 | }
162 |
163 | // 加载错误
164 | if (indexScreenVideoModel.error) {
165 | Box(
166 | modifier = Modifier
167 | .fillMaxWidth()
168 | .height(120.dp), contentAlignment = Alignment.Center
169 | ) {
170 | Column {
171 | Text(text = "加载错误!😨", fontWeight = FontWeight.Bold)
172 | Text(text = "请检查你的网络连接,或者可能是查重服务器维护中")
173 | }
174 | }
175 | }
176 |
177 | // 结果
178 | response?.let {
179 | when (it.code) {
180 | 0 -> {
181 | Box(
182 | modifier = Modifier
183 | .fillMaxWidth()
184 | .padding(16.dp),
185 | //elevation = 4.dp
186 | ) {
187 | Column(
188 | modifier = Modifier.padding(16.dp),
189 | horizontalAlignment = Alignment.CenterHorizontally
190 | ) {
191 | Row(
192 | Modifier.fillMaxWidth(),
193 | verticalAlignment = Alignment.CenterVertically
194 | ) {
195 | Text(
196 | text = "总文字复制比: ${(it.data.rate * 100).format()}%",
197 | fontWeight = FontWeight.Bold,
198 | fontSize = 23.sp,
199 | modifier = Modifier.padding(4.dp),
200 | color = MaterialTheme.colors.secondary
201 | )
202 | CircularProgressIndicator(
203 | modifier = Modifier
204 | .padding(horizontal = 16.dp)
205 | .size(25.dp),
206 | progress = it.data.rate.toFloat()
207 | )
208 | }
209 | Spacer(modifier = Modifier.height(8.dp))
210 | // "复制查重结果"按钮
211 | OutlinedButton(modifier = Modifier.fillMaxWidth(), onClick = {
212 | val clipboardManager =
213 | context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
214 | clipboardManager.setPrimaryClip(
215 | ClipData.newPlainText(
216 | null, """
217 | 枝网文本复制检测报告(APP版)
218 | 查重时间: ${getDateTime(System.currentTimeMillis())}
219 | 总文字复制比: ${(it.data.rate * 100).format()}%
220 | 相似小作文: ${it.data.related[0].replyUrl}
221 | 作者: ${it.data.related[0].reply.mName}
222 | 发表时间: ${getDateTime(it.data.related[0].reply.ctime.toLong() * 1000L)}
223 |
224 | 查重结果仅作参考,请注意辨别是否为原创
225 | """.trimIndent()
226 | )
227 | )
228 | Toast.makeText(context, "已复制到剪贴板", Toast.LENGTH_SHORT).show()
229 | }) {
230 | Text(text = "点击复制查重结果")
231 | }
232 | CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
233 | // 防止某些乱复制的
234 | Text(text = "请自行判别是否原创,请勿到处刷查重报告")
235 | Text(text = "A友们都是有素质的人捏")
236 | }
237 | }
238 | }
239 | // 查重结果概述
240 | Text(
241 | text = "相似小作文: (${it.data.related.size}篇)",
242 | fontWeight = FontWeight.Bold,
243 | modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
244 | )
245 | // 相似小作文列表
246 |
247 | it.data.related.forEach { zuowen ->
248 | XiaoZuoWen(zuowen)
249 | }
250 | }
251 | 4003 -> {
252 | Text(text = "服务器内部错误")
253 | }
254 | }
255 | }
256 |
257 | Spacer(
258 | modifier = Modifier
259 | .padding(vertical = 8.dp)
260 | .fillMaxWidth()
261 | .height(0.5.dp)
262 | .background(Color.Gray)
263 | )
264 | Text(
265 | modifier = Modifier.fillMaxWidth(),
266 | text = "数据来源于: https://asoulcnki.asia/",
267 | textAlign = TextAlign.Center
268 | )
269 | Text(
270 | modifier = Modifier.fillMaxWidth(),
271 | text = "目前仅收录了官方账号下面的小作文,二创下的小作文并未收录,所以查重结果仅供参考哦",
272 | textAlign = TextAlign.Center
273 | )
274 | }
275 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/ui/screen/index/page/MemeWiki.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.ui.screen.index.page
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.foundation.lazy.items
9 | import androidx.compose.material.Card
10 | import androidx.compose.material.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.text.font.FontWeight
16 | import androidx.compose.ui.unit.dp
17 | import androidx.compose.ui.unit.sp
18 | import androidx.navigation.NavController
19 | import com.google.accompanist.placeholder.PlaceholderHighlight
20 | import com.google.accompanist.placeholder.material.placeholder
21 | import com.google.accompanist.placeholder.material.shimmer
22 | import com.google.accompanist.swiperefresh.SwipeRefresh
23 | import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
24 | import me.rerere.zhiwang.api.wiki.WikiList
25 | import me.rerere.zhiwang.ui.screen.index.IndexScreenVideoModel
26 | import me.rerere.zhiwang.util.noRippleClickable
27 |
28 | @Composable
29 | fun WikiPage(navController: NavController, indexViewModel: IndexScreenVideoModel) {
30 | val context = LocalContext.current
31 | when {
32 | indexViewModel.wikiLoading -> {
33 | Column {
34 | repeat(7) {
35 | Box(
36 | modifier = Modifier
37 | .padding(16.dp)
38 | .fillMaxWidth()
39 | .height(70.dp)
40 | .placeholder(visible = true, highlight = PlaceholderHighlight.shimmer())
41 | )
42 | }
43 | }
44 | }
45 | indexViewModel.wikiError -> {
46 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
47 | Column(horizontalAlignment = Alignment.CenterHorizontally) {
48 | Text(
49 | text = "加载失败,点击重新加载",
50 | fontWeight = FontWeight.Bold,
51 | modifier = Modifier.noRippleClickable {
52 | indexViewModel.loadWiki()
53 | })
54 | }
55 | }
56 | }
57 | else -> {
58 | SwipeRefresh(state = rememberSwipeRefreshState(isRefreshing = indexViewModel.wikiLoading), onRefresh = { indexViewModel.loadWiki() }) {
59 | LazyColumn(Modifier.fillMaxSize()) {
60 | item {
61 | Card(
62 | modifier = Modifier
63 | .fillMaxWidth()
64 | .padding(16.dp)
65 | .clickable {
66 | val intent = Intent(
67 | Intent.ACTION_VIEW,
68 | Uri.parse(("https://github.com/jiangdashao/asoulzhiwang").trim())
69 | )
70 | context.startActivity(intent)
71 | },
72 | ) {
73 | Column(Modifier.padding(16.dp)) {
74 | Text(text = "声明", fontWeight = FontWeight.Bold, fontSize = 20.sp)
75 | Spacer(modifier = Modifier.height(10.dp))
76 | Text(text = "本WIKI中许多梗介绍来自 深紫色的白")
77 | Text(text = "如果你想添加梗介绍,请前往github提交对 wiki.json 的PR")
78 | Text(text = "https://github.com/jiangdashao/asoulzhiwang")
79 | }
80 | }
81 | }
82 | items(indexViewModel.wikiList){
83 | WikiItem(it)
84 | }
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
91 | @Composable
92 | private fun WikiItem(wikiListItem: WikiList.WikiListItem) {
93 | Card(
94 | modifier = Modifier
95 | .fillMaxWidth()
96 | .padding(16.dp),
97 | ) {
98 | Column(Modifier.padding(16.dp)) {
99 | Text(text = wikiListItem.title, fontWeight = FontWeight.Bold, fontSize = 20.sp)
100 | Spacer(modifier = Modifier.height(10.dp))
101 | Text(text = wikiListItem.description)
102 | }
103 | }
104 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/ui/screen/index/page/Zuowen.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.ui.screen.index.page
2 |
3 | import android.util.Log
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.material.*
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.CompositionLocalProvider
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.text.font.FontWeight
16 | import androidx.compose.ui.unit.dp
17 | import androidx.compose.ui.unit.sp
18 | import androidx.navigation.NavController
19 | import androidx.navigation.NavType
20 | import androidx.navigation.Navigator
21 | import androidx.paging.LoadState
22 | import androidx.paging.compose.collectAsLazyPagingItems
23 | import androidx.paging.compose.items
24 | import com.google.accompanist.placeholder.PlaceholderHighlight
25 | import com.google.accompanist.placeholder.material.fade
26 | import com.google.accompanist.placeholder.material.placeholder
27 | import com.google.accompanist.swiperefresh.SwipeRefresh
28 | import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
29 | import me.rerere.zhiwang.api.zuowen.ZuowenResponse
30 | import me.rerere.zhiwang.ui.screen.index.IndexScreenVideoModel
31 | import me.rerere.zhiwang.util.noRippleClickable
32 | import java.lang.StringBuilder
33 |
34 | @Composable
35 | fun Zuowen(indexScreenVideoModel: IndexScreenVideoModel, navController: NavController) {
36 | val articleList = indexScreenVideoModel.pager.collectAsLazyPagingItems()
37 | SwipeRefresh(
38 | state = rememberSwipeRefreshState(articleList.loadState.refresh == LoadState.Loading),
39 | onRefresh = { articleList.refresh() }) {
40 | LazyColumn(Modifier.fillMaxSize()) {
41 | if(articleList.loadState.refresh is LoadState.Error) {
42 | item {
43 | Column(modifier = Modifier.fillMaxWidth().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
44 | Text(text = "加载失败,下拉重试 😱")
45 | }
46 | }
47 | }
48 |
49 | if(articleList.loadState.refresh == LoadState.Loading && articleList.itemCount == 0){
50 | repeat(10){
51 | item {
52 | Box(
53 | modifier = Modifier
54 | .padding(16.dp)
55 | .fillMaxWidth()
56 | .height(70.dp)
57 | .placeholder(visible = true, highlight = PlaceholderHighlight.fade())
58 | )
59 | }
60 | }
61 | }
62 |
63 | items(articleList) {
64 | Article(it!!, navController)
65 | }
66 |
67 | when (articleList.loadState.append) {
68 | LoadState.Loading -> {
69 | item {
70 | Box(
71 | modifier = Modifier.fillMaxWidth(),
72 | contentAlignment = Alignment.Center
73 | ) {
74 | CircularProgressIndicator()
75 | }
76 | }
77 | }
78 | is LoadState.Error -> {
79 | item {
80 | Box(
81 | modifier = Modifier
82 | .fillMaxWidth()
83 | .noRippleClickable {
84 | articleList.retry()
85 | }, contentAlignment = Alignment.Center
86 | ) {
87 | Text(text = "加载更多页面失败,点击重试!", fontWeight = FontWeight.Bold)
88 | }
89 | }
90 | }
91 | else -> {}
92 | }
93 | }
94 | }
95 | }
96 |
97 | @Composable
98 | private fun Article(article: ZuowenResponse.Article, navController: NavController) {
99 | val context = LocalContext.current
100 | Card(
101 | modifier = Modifier
102 | .fillMaxWidth()
103 | .padding(16.dp)
104 | .clickable {
105 | navController.navigate("zuowen?id=${article.id}&title=${article.title}&author=${article.author}&tags=${article.tags.joinToString(",")}")
106 | },
107 | elevation = 4.dp
108 | ) {
109 | Column(Modifier.padding(16.dp)) {
110 | // 标题
111 | Text(text = article.title, fontWeight = FontWeight.Bold, fontSize = 20.sp)
112 | // 作者
113 | CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
114 | Text(text = article.author)
115 | }
116 | // 分割线
117 | Spacer(
118 | modifier = Modifier
119 | .padding(horizontal = 4.dp, vertical = 4.dp)
120 | .fillMaxWidth()
121 | .height(0.5.dp)
122 | .background(Color.LightGray)
123 | )
124 | // 内容
125 | Text(text = article.plainContent)
126 | // 分割线
127 | Spacer(
128 | modifier = Modifier
129 | .padding(horizontal = 4.dp, vertical = 4.dp)
130 | .fillMaxWidth()
131 | .height(0.5.dp)
132 | .background(Color.LightGray)
133 | )
134 | // 标签
135 | Row {
136 | article.tags.forEach {
137 | Text(
138 | text = it,
139 | color = MaterialTheme.colors.secondary,
140 | modifier = Modifier
141 | .padding(vertical = 2.dp, horizontal = 4.dp)
142 | )
143 | }
144 | }
145 | }
146 | }
147 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/ui/screen/zuowen/ZuowenScreen.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.ui.screen.zuowen
2 |
3 | import android.content.ClipData
4 | import android.content.ClipboardManager
5 | import android.content.Context
6 | import android.util.Log
7 | import android.widget.Toast
8 | import androidx.compose.foundation.background
9 | import androidx.compose.foundation.layout.*
10 | import androidx.compose.foundation.rememberScrollState
11 | import androidx.compose.foundation.text.selection.SelectionContainer
12 | import androidx.compose.foundation.verticalScroll
13 | import androidx.compose.material.Icon
14 | import androidx.compose.material.IconButton
15 | import androidx.compose.material.Scaffold
16 | import androidx.compose.material.Text
17 | import androidx.compose.material.icons.Icons
18 | import androidx.compose.material.icons.filled.ArrowBack
19 | import androidx.compose.material.icons.filled.ContentCopy
20 | import androidx.compose.material.icons.filled.FindReplace
21 | import androidx.compose.runtime.*
22 | import androidx.compose.ui.Alignment
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.graphics.Color
25 | import androidx.compose.ui.platform.LocalContext
26 | import androidx.compose.ui.text.font.FontWeight
27 | import androidx.compose.ui.unit.dp
28 | import androidx.compose.ui.unit.sp
29 | import androidx.hilt.navigation.compose.hiltViewModel
30 | import androidx.navigation.NavController
31 | import com.google.accompanist.insets.LocalWindowInsets
32 | import com.google.accompanist.insets.navigationBarsPadding
33 | import com.google.accompanist.insets.rememberInsetsPaddingValues
34 | import com.google.accompanist.insets.ui.TopAppBar
35 | import com.google.accompanist.placeholder.PlaceholderHighlight
36 | import com.google.accompanist.placeholder.material.placeholder
37 | import com.google.accompanist.placeholder.material.shimmer
38 | import com.vanpra.composematerialdialogs.MaterialDialog
39 | import com.vanpra.composematerialdialogs.listItemsSingleChoice
40 | import com.vanpra.composematerialdialogs.rememberMaterialDialogState
41 | import com.vanpra.composematerialdialogs.title
42 | import me.rerere.zhiwang.util.MEMBERS
43 | import me.rerere.zhiwang.util.replaceASoulMemberName
44 |
45 | private const val TAG = "ZuowenScreen"
46 |
47 | @Composable
48 | fun ZuowenScreen(
49 | navController: NavController,
50 | id: String,
51 | title: String,
52 | author: String,
53 | tags: List,
54 | zuowenViewModel: ZuowenViewModel = hiltViewModel()
55 | ) {
56 | val context = LocalContext.current
57 | LaunchedEffect(Unit) {
58 | Log.i(TAG, "ZuowenScreen: Load = $id, $title, $author")
59 | zuowenViewModel.load(id, title, author)
60 | }
61 | val replaceDialog = rememberMaterialDialogState()
62 | var replaceSelection by remember {
63 | mutableStateOf(0)
64 | }
65 | MaterialDialog(
66 | dialogState = replaceDialog,
67 | buttons = {
68 | button("复制") {
69 | replaceDialog.hide()
70 | val clipboardManager =
71 | context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
72 | clipboardManager.setPrimaryClip(
73 | ClipData.newPlainText(
74 | null,
75 | zuowenViewModel.content.replaceASoulMemberName(tags[0], MEMBERS[replaceSelection])
76 | )
77 | )
78 | Toast.makeText(context, "已复制到剪贴板", Toast.LENGTH_SHORT).show()
79 | }
80 | }
81 | ) {
82 | title("快速替换小作文的主语并复制到剪贴板")
83 | listItemsSingleChoice(
84 | list = MEMBERS,
85 | initialSelection = replaceSelection,
86 | onChoiceChange = {
87 | replaceSelection = it
88 | },
89 | waitForPositiveButton = false
90 | )
91 | }
92 |
93 | Scaffold(
94 | topBar = {
95 | TopAppBar(
96 | contentPadding = rememberInsetsPaddingValues(insets = LocalWindowInsets.current.statusBars, applyBottom = false),
97 | navigationIcon = {
98 | IconButton(onClick = { navController.popBackStack() }) {
99 | Icon(Icons.Default.ArrowBack, null)
100 | }
101 | },
102 | title = {
103 | Text(text = "阅读小作文")
104 | },
105 | actions = {
106 | // 直接复制小作文
107 | IconButton(onClick = {
108 | if (!zuowenViewModel.error && !zuowenViewModel.loading) {
109 | val clipboardManager =
110 | context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
111 | clipboardManager.setPrimaryClip(
112 | ClipData.newPlainText(
113 | null, zuowenViewModel.content
114 | )
115 | )
116 | Toast.makeText(context, "已复制到剪贴板", Toast.LENGTH_SHORT).show()
117 | }
118 | }) {
119 | Icon(Icons.Default.ContentCopy, null)
120 | }
121 |
122 | if (tags.size == 1) {
123 | // 小作文主语替换复制
124 | IconButton(onClick = {
125 | replaceDialog.show()
126 | }) {
127 | Icon(Icons.Default.FindReplace, null)
128 | }
129 | }
130 | }
131 | )
132 | }
133 | ) {
134 | Box(
135 | modifier = Modifier
136 | .fillMaxSize()
137 | .padding(it)
138 | .navigationBarsPadding()
139 | ) {
140 | ZuowenContent(zuowenViewModel)
141 | }
142 | }
143 | }
144 |
145 | @Composable
146 | private fun ZuowenContent(zuowenViewModel: ZuowenViewModel) {
147 | when {
148 | // 加载中
149 | zuowenViewModel.loading -> {
150 | Column(Modifier.fillMaxWidth()) {
151 | repeat(8) {
152 | Box(
153 | modifier = Modifier
154 | .padding(16.dp)
155 | .fillMaxWidth()
156 | .height(70.dp)
157 | .placeholder(visible = true, highlight = PlaceholderHighlight.shimmer())
158 | )
159 | }
160 | }
161 | }
162 | // 加载错误
163 | zuowenViewModel.error -> {
164 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
165 | Text(text = "加载失败 😨", fontWeight = FontWeight.Bold)
166 | }
167 | }
168 | // 加载完成
169 | else -> {
170 | ArticleContent(zuowenViewModel)
171 | }
172 | }
173 | }
174 |
175 | @Composable
176 | private fun ArticleContent(zuowenViewModel: ZuowenViewModel) {
177 | Column(
178 | Modifier
179 | .padding(16.dp)
180 | .verticalScroll(rememberScrollState())
181 | ) {
182 | Text(text = zuowenViewModel.title, fontSize = 25.sp, fontWeight = FontWeight.Bold)
183 | Text(text = zuowenViewModel.author)
184 | Spacer(modifier = Modifier.height(10.dp))
185 | Spacer(
186 | modifier = Modifier
187 | .padding(horizontal = 4.dp, vertical = 4.dp)
188 | .fillMaxWidth()
189 | .height(0.5.dp)
190 | .background(Color.LightGray)
191 | )
192 | // 显示作文内容
193 | SelectionContainer {
194 | Column {
195 | zuowenViewModel.content.split("\n").forEach {
196 | Box(Modifier.padding(vertical = 4.dp)) {
197 | Text(text = " $it")
198 | }
199 | }
200 | }
201 | }
202 | }
203 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/ui/screen/zuowen/ZuowenViewModel.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.ui.screen.zuowen
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.launch
10 | import me.rerere.zhiwang.repo.ZuowenRepo
11 | import me.rerere.zhiwang.util.format.HtmlFormatUtil
12 | import org.jsoup.Jsoup
13 | import javax.inject.Inject
14 |
15 |
16 | @HiltViewModel
17 | class ZuowenViewModel @Inject constructor(
18 | private val zuowenRepo: ZuowenRepo
19 | ) : ViewModel() {
20 | var loading by mutableStateOf(false)
21 | var error by mutableStateOf(false)
22 |
23 | // article info
24 | var id by mutableStateOf("")
25 | var title by mutableStateOf("")
26 | var author by mutableStateOf("")
27 | var content by mutableStateOf("")
28 |
29 | // 加载小作文详细内容
30 | fun load(id: String, title: String, author: String) {
31 |
32 | this.id = id
33 | this.title = title
34 | this.author = author
35 |
36 | viewModelScope.launch {
37 | loading = true
38 | error = false
39 | val content = zuowenRepo.getZuowenContent(id)
40 | content?.let {
41 | // 利用JSOUP移除HTML元素
42 | this@ZuowenViewModel.content =
43 | HtmlFormatUtil.getPlainText(Jsoup.parse(it.htmlContent).root())
44 | ?.replace(regex = Regex("[\\r\\n]+"), replacement = "\n")
45 | ?.trim()
46 | ?: "解析错误"
47 | } ?: kotlin.run {
48 | // 加载错误
49 | error = true
50 | println("加载小作文内容失败")
51 | }
52 |
53 | loading = false
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val PINK = Color(0xfff45a8d)
6 | val BACKGROUND = Color(0xFFF2F3F5)
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material.Colors
6 | import androidx.compose.material.MaterialTheme
7 | import androidx.compose.material.darkColors
8 | import androidx.compose.material.lightColors
9 | import androidx.compose.material3.*
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.platform.LocalContext
13 |
14 | private val DarkColorPalette = darkColors(
15 | primary = Color(0xff05aa85),
16 | secondary = Color(0xffaa0529)
17 | )
18 |
19 | private val LightColorPalette = lightColors(
20 | primary = Color(0xff05aa85),
21 | secondary = Color(0xffaa0529)
22 | )
23 |
24 | val Colors.uiBackGroundColor
25 | get() = if(isLight){
26 | Color.White
27 | } else {
28 | Color.Black
29 | }
30 |
31 | @Composable
32 | fun md3Color(
33 | darkTheme: Boolean
34 | ): ColorScheme {
35 | val context = LocalContext.current
36 | return if (darkTheme) {
37 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
38 | dynamicDarkColorScheme(context)
39 | } else {
40 | darkColorScheme()
41 | }
42 | } else {
43 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
44 | dynamicLightColorScheme(context)
45 | }else {
46 | lightColorScheme()
47 | }
48 | }
49 | }
50 |
51 | @Composable
52 | fun ZhiWangTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
53 | val colors = if (darkTheme) {
54 | DarkColorPalette
55 | } else {
56 | LightColorPalette
57 | }
58 | androidx.compose.material3.MaterialTheme(
59 | colorScheme = md3Color(darkTheme)
60 | ) {
61 | MaterialTheme(
62 | colors = colors,
63 | typography = Typography,
64 | shapes = Shapes,
65 | content = content
66 | )
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/ModifierEx.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.interaction.MutableInteractionSource
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.composed
9 |
10 | @Composable
11 | inline fun Modifier.noRippleClickable(crossinline onClick: ()->Unit): Modifier = composed {
12 | clickable(indication = null,
13 | interactionSource = remember { MutableInteractionSource() }) {
14 | onClick()
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/ReplaceUtil.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util
2 |
3 | val MEMBERS = listOf(
4 | "嘉然",
5 | "向晚",
6 | "乃琳",
7 | "珈乐",
8 | "贝拉"
9 | )
10 |
11 | fun String.replaceASoulMemberName(fromName: String, toName: String): String {
12 | return replace(fromName, toName)
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/UpdateChecker.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util
2 |
3 | import android.content.Context
4 | import android.content.pm.PackageInfo
5 | import android.content.pm.PackageManager
6 | import android.os.Build
7 | import android.util.Log
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.withContext
10 | import me.rerere.zhiwang.util.net.await
11 | import okhttp3.CacheControl
12 | import okhttp3.OkHttpClient
13 | import okhttp3.Request
14 |
15 | private const val TAG = "UpdateChecker"
16 |
17 | suspend fun checkUpdate(context: Context): Boolean {
18 | return withContext(Dispatchers.IO) {
19 | try {
20 | val okHttpClient = OkHttpClient()
21 | val request = Request.Builder()
22 | .cacheControl(CacheControl.Builder().noCache().build())
23 | .url("https://cdn.jsdelivr.net/gh/jiangdashao/ASoulZhiWang/app/build.gradle.kts")
24 | .get()
25 | .build()
26 | val response = okHttpClient.newCall(request).await()
27 | require(response.isSuccessful)
28 | val content = response.body!!.string()
29 | require(content.isNotEmpty())
30 | val latestVersion = content.let {
31 | it.substring(
32 | it.indexOf("versionCode = ") + "versionCode = ".length,
33 | it.indexOf(char = '\n', startIndex = it.indexOf("versionCode"))
34 | )
35 | }
36 | val latestVersionCode = latestVersion.toInt()
37 | val currentVersionCode = getAppVersionCode(context)
38 | Log.i(TAG, "checkUpdate: Latest: $latestVersionCode")
39 | if (latestVersionCode > currentVersionCode) {
40 | Log.i(
41 | TAG,
42 | "checkUpdate: Found a update! (current: $currentVersionCode, latest: $latestVersionCode)"
43 | )
44 | } else {
45 | Log.i(TAG, "checkUpdate: There is no update")
46 | }
47 | latestVersionCode > currentVersionCode
48 | } catch (e: Exception) {
49 | e.printStackTrace()
50 | false
51 | }
52 | }
53 | }
54 |
55 | fun getAppVersionCode(context: Context): Long {
56 | var appVersionCode: Long = 0
57 | try {
58 | val packageInfo: PackageInfo = context.applicationContext
59 | .packageManager
60 | .getPackageInfo(context.packageName, 0)
61 | appVersionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
62 | packageInfo.longVersionCode
63 | } else {
64 | packageInfo.versionCode.toLong()
65 | }
66 | } catch (e: PackageManager.NameNotFoundException) {
67 | e.printStackTrace()
68 | }
69 | return appVersionCode
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/android/ClipboardUtil.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.android
2 |
3 | import android.content.ClipData
4 | import android.content.ClipboardManager
5 | import android.content.Context
6 |
7 | fun Context.getClipboardContent(): String? {
8 | val clipboardManager = this.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
9 | clipboardManager.let {
10 | if (it.hasPrimaryClip() && it.primaryClip!!.itemCount > 0) {
11 | val text = it.primaryClip!!.getItemAt(0).text
12 | if (text.length >= 10) {
13 | return text.toString()
14 | }
15 | }
16 | }
17 | return null
18 | }
19 |
20 | fun Context.setClipboardText(text: String) {
21 | val clipboardManager = this.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
22 | clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text))
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/ChartShape.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts
2 |
3 | import androidx.compose.foundation.shape.CircleShape
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.compose.ui.graphics.Shape
6 | import androidx.compose.ui.unit.Dp
7 | import androidx.compose.ui.unit.dp
8 |
9 | data class ChartShape(
10 | val size: Dp,
11 | val color: Color,
12 | val shape: Shape,
13 | ) {
14 | companion object {
15 | val Default = ChartShape(
16 | size = 8.dp,
17 | color = Color.Cyan,
18 | shape = CircleShape
19 | )
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/bars/DropdownContent.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.bars
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.foundation.shape.CircleShape
6 | import androidx.compose.material.MaterialTheme
7 | import androidx.compose.material.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.unit.dp
14 | import me.rerere.zhiwang.util.charts.bars.data.StackedBarData
15 | import me.rerere.zhiwang.util.charts.bars.data.StackedBarItem
16 |
17 | sealed class PopupState {
18 | object Idle : PopupState()
19 | data class Showing(val data: StackedBarData) : PopupState()
20 | }
21 |
22 | @Composable
23 | internal fun DropdownContent(
24 | colors: List = emptyList(),
25 | data: StackedBarData,
26 | ) {
27 | val entries = remember(data) {
28 | data.entries.mapIndexed { idx, item ->
29 | StackedBarItem(
30 | text = item.text,
31 | value = item.value,
32 | color = item.color ?: colors[idx]
33 | )
34 | }
35 | }
36 |
37 | Text(
38 | data.title,
39 | style = MaterialTheme.typography.caption,
40 | )
41 |
42 | Spacer(
43 | modifier = Modifier
44 | .fillMaxWidth()
45 | .requiredHeight(8.dp)
46 | )
47 |
48 | entries.forEachIndexed { idx, (text, value, color) ->
49 | Row(Modifier.requiredHeight(20.dp), verticalAlignment = Alignment.CenterVertically) {
50 | Box(
51 | Modifier
52 | .requiredSize(8.dp)
53 | .background(color, CircleShape)
54 | )
55 |
56 | Text(
57 | text = text,
58 | modifier = Modifier.padding(start = 8.dp),
59 | style = MaterialTheme.typography.caption,
60 | )
61 |
62 | Spacer(
63 | modifier = Modifier
64 | .requiredHeight(20.dp)
65 | .weight(1f)
66 | )
67 |
68 | Text(
69 | text = value.toInt().toString(),
70 | style = MaterialTheme.typography.caption,
71 | )
72 | }
73 |
74 | if (idx != entries.lastIndex)
75 | Spacer(
76 | modifier = Modifier
77 | .fillMaxWidth()
78 | .requiredHeight(8.dp)
79 | )
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/bars/HorizontalBarsChart.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.bars
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.interaction.MutableInteractionSource
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.material.DropdownMenu
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.layout.layout
12 | import androidx.compose.ui.platform.LocalDensity
13 | import androidx.compose.ui.text.AnnotatedString
14 | import androidx.compose.ui.unit.Dp
15 | import androidx.compose.ui.unit.DpOffset
16 | import androidx.compose.ui.unit.dp
17 | import me.rerere.zhiwang.util.charts.bars.data.HorizontalBarsData
18 | import me.rerere.zhiwang.util.charts.bars.data.StackedBarData
19 | import me.rerere.zhiwang.util.charts.bars.data.StackedBarItem
20 | import me.rerere.zhiwang.util.charts.internal.DefaultText
21 | import me.rerere.zhiwang.util.charts.internal.safeGet
22 | import me.rerere.zhiwang.util.charts.legend.DrawHorizontalLegend
23 | import me.rerere.zhiwang.util.charts.legend.LegendEntry
24 | import kotlin.math.min
25 |
26 | internal val MinimumBarWidth = 24.dp
27 | internal val OptionalBarOffset = 75.dp
28 | internal val MaximumBarWidth = 275.dp
29 |
30 | typealias TextRowFactory = @Composable RowScope.(title: AnnotatedString) -> Unit
31 |
32 | internal val DropdownDefaultModifier = Modifier
33 | .requiredWidth(176.dp)
34 | .padding(16.dp)
35 |
36 | @Composable
37 | fun HorizontalBarsChart(
38 | data: HorizontalBarsData,
39 | modifier: Modifier = Modifier,
40 | divider: @Composable (() -> Unit)? = null,
41 | legend: @Composable (ColumnScope.(entries: List) -> Unit)? = null,
42 | legendOffset: Dp = 4.dp,
43 | dropdownModifier: Modifier = DropdownDefaultModifier,
44 | dropdownContent: @Composable (StackedBarData) -> Unit = {
45 | DropdownContent(data = it, colors = data.colors)
46 | },
47 | textContent: TextRowFactory = { DefaultText(text = it) },
48 | valueContent: TextRowFactory = { DefaultText(text = it) },
49 | ) {
50 | val legendEntries = remember(data) {
51 | data.customLegendEntries.takeIf { it.isNotEmpty() } ?: data.legendEntries()
52 | }
53 | var popupState: PopupState by remember { mutableStateOf(PopupState.Idle) }
54 |
55 | Column(modifier = modifier) {
56 | if (legend != null) {
57 | legend(legendEntries)
58 | } else {
59 | DrawHorizontalLegend(legendEntries = legendEntries)
60 | }
61 |
62 | Spacer(
63 | modifier = Modifier
64 | .fillMaxWidth()
65 | .requiredHeight(legendOffset)
66 | )
67 |
68 | val maxValue = data.bars.maxByOrNull { bar -> bar.count }?.count ?: 0
69 |
70 | data.bars.forEachIndexed { idx, bar ->
71 | Box {
72 | StackedHorizontalBar(
73 | modifier = Modifier.clickable(
74 | interactionSource = MutableInteractionSource(),
75 | indication = null,
76 | enabled = data.isPopupEnabled,
77 | onClick = { popupState = PopupState.Showing(bar) },
78 | ),
79 | colors = data.colors,
80 | data = bar,
81 | maxBarValue = maxValue,
82 | shouldDrawDivider = idx != data.bars.lastIndex,
83 | divider = divider,
84 | title = textContent,
85 | value = valueContent,
86 | )
87 |
88 | DropdownMenu(
89 | modifier = dropdownModifier,
90 | offset = DpOffset(0.dp, (-48).dp),
91 | expanded = popupState.let { it is PopupState.Showing && it.data == bar },
92 | onDismissRequest = { popupState = PopupState.Idle }
93 | ) {
94 | popupState.let {
95 | if (it is PopupState.Showing)
96 | dropdownContent(it.data)
97 | }
98 | }
99 | }
100 | }
101 | }
102 | }
103 |
104 | @Composable
105 | internal fun StackedHorizontalBar(
106 | modifier: Modifier = Modifier,
107 | colors: List,
108 | data: StackedBarData,
109 | maxBarValue: Int,
110 | shouldDrawDivider: Boolean,
111 | divider: @Composable (() -> Unit)? = null,
112 | title: TextRowFactory = {},
113 | value: TextRowFactory = {},
114 | ) {
115 | val entries = data.entries.mapIndexed { idx, item ->
116 | StackedBarItem(
117 | text = item.text,
118 | value = item.value,
119 | color = item.color ?: colors.safeGet(idx)
120 | )
121 | }
122 |
123 | Column(modifier) {
124 | Spacer(
125 | modifier = Modifier
126 | .fillMaxWidth()
127 | .requiredHeight(12.dp)
128 | )
129 | Row(
130 | modifier = Modifier.heightIn(min = 24.dp),
131 | verticalAlignment = Alignment.CenterVertically
132 | ) {
133 | Row(
134 | // Trim the width of text to [container.width - 50.dp]
135 | Modifier.layout { measurable, constraints ->
136 | val width = constraints.maxWidth - 50.dp.roundToPx()
137 | val placeable = measurable.measure(constraints.copy(maxWidth = width, minWidth = width))
138 | layout(width, placeable.height) {
139 | placeable.placeRelative(0, 0)
140 | }
141 | }
142 | ) {
143 | title(data.title)
144 | }
145 |
146 | Spacer(
147 | modifier = Modifier.weight(1f)
148 | )
149 |
150 | value(AnnotatedString(data.count.toString()))
151 | }
152 |
153 | Spacer(
154 | modifier = Modifier
155 | .fillMaxWidth()
156 | .requiredHeight(4.dp)
157 | )
158 |
159 | BoxWithConstraints {
160 | val rightPadding = with(LocalDensity.current) { OptionalBarOffset.roundToPx() }
161 | val maxWidthPx = with(LocalDensity.current) { MaximumBarWidth.roundToPx() }
162 | val maxBarWidthPx = min(maxWidthPx, constraints.maxWidth - rightPadding)
163 | val minWidthPx = with(LocalDensity.current) { MinimumBarWidth.roundToPx() }
164 |
165 | val percentage = data.count / maxBarValue.toFloat()
166 | val width = maxBarWidthPx * percentage
167 |
168 | DrawStackedBar(
169 | entries = entries,
170 | widthPx = if (width < minWidthPx) width + minWidthPx else width
171 | )
172 | }
173 |
174 | Spacer(
175 | modifier = Modifier
176 | .fillMaxWidth()
177 | .requiredHeight(12.dp)
178 | )
179 |
180 | if (shouldDrawDivider && divider != null) {
181 | divider()
182 | }
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/bars/StackedHorizontalBar.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.bars
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.draw.drawBehind
8 | import androidx.compose.ui.geometry.Size
9 | import androidx.compose.ui.graphics.Path
10 | import androidx.compose.ui.platform.LocalDensity
11 | import androidx.compose.ui.unit.dp
12 | import me.rerere.zhiwang.util.charts.bars.data.StackedBarItem
13 |
14 | internal typealias EntryPathFactory = (entry: EntryDrawShape, size: Size) -> Path
15 |
16 | internal val EntrySpacing = 2.dp
17 |
18 | @Composable
19 | internal fun DrawStackedBar(
20 | entries: List,
21 | widthPx: Float,
22 | entryPathFactory: EntryPathFactory = { entry, size -> createBarEntryShape(entry, size) },
23 | ) {
24 | val total = remember(entries) { entries.sumByDouble { it.value.toDouble() }.toFloat() }
25 | val width = with(LocalDensity.current) { widthPx.toDp() }
26 | val spacingPx = with(LocalDensity.current) { EntrySpacing.roundToPx() }
27 |
28 | val totalSpacing = (entries.size - 1) * spacingPx
29 |
30 | val values = remember(entries) {
31 | entries.map {
32 | it.copy(value = (widthPx - totalSpacing) * it.value / total)
33 | }
34 | }
35 |
36 | Row(
37 | modifier = Modifier.requiredWidth(width),
38 | horizontalArrangement = Arrangement.spacedBy(EntrySpacing)
39 | ) {
40 | values.forEachIndexed { idx, item ->
41 | val shape = when {
42 | idx == 0 && values.size == 1 -> EntryDrawShape.Single
43 | idx == 0 -> EntryDrawShape.First
44 | idx == values.lastIndex -> EntryDrawShape.Last
45 | else -> EntryDrawShape.Middle
46 | }
47 |
48 | Box(
49 | modifier = Modifier
50 | .requiredWidth(with(LocalDensity.current) { item.value.toDp() })
51 | .requiredHeight(8.dp)
52 | .drawBehind {
53 | drawPath(entryPathFactory(shape, size), item.color)
54 | }
55 | )
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/bars/Utils.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.bars
2 |
3 | import androidx.compose.foundation.shape.CircleShape
4 | import androidx.compose.ui.geometry.*
5 | import androidx.compose.ui.graphics.Path
6 | import androidx.compose.ui.unit.dp
7 | import me.rerere.zhiwang.util.charts.ChartShape
8 | import me.rerere.zhiwang.util.charts.bars.data.HorizontalBarsData
9 | import me.rerere.zhiwang.util.charts.internal.safeGet
10 | import me.rerere.zhiwang.util.charts.legend.LegendEntry
11 |
12 | internal fun HorizontalBarsData.uniqueBarEntries() =
13 | bars.asSequence()
14 | .map { bar -> bar.entries }
15 | .flatten()
16 | .distinctBy { it.text }
17 | .toList()
18 |
19 | internal fun HorizontalBarsData.legendEntries() = uniqueBarEntries().mapIndexed { idx, item ->
20 | LegendEntry(
21 | text = item.text,
22 | value = item.value,
23 | percent = 0f,
24 | shape = ChartShape(
25 | size = 8.dp,
26 | color = item.color ?: colors.safeGet(idx),
27 | shape = CircleShape,
28 | )
29 | )
30 | }
31 |
32 | internal enum class EntryDrawShape {
33 | // Rounded on all sides
34 | Single,
35 |
36 | // Rounded left side
37 | First,
38 |
39 | // Not rounded
40 | Middle,
41 |
42 | // Rounded right side
43 | Last;
44 | }
45 |
46 | // TODO: make this customisable
47 | internal fun createBarEntryShape(shape: EntryDrawShape, size: Size): Path {
48 | val rect = Rect(Offset(0f, 0f), Offset(size.width, size.height))
49 | return Path().apply {
50 | when (shape) {
51 | EntryDrawShape.Single -> addRoundRect(
52 | RoundRect(
53 | rect = rect,
54 | cornerRadius = CornerRadius(size.height)
55 | )
56 | )
57 | EntryDrawShape.First -> addRoundRect(
58 | RoundRect(
59 | rect = rect,
60 | topRight = CornerRadius.Zero,
61 | bottomRight = CornerRadius.Zero,
62 | topLeft = CornerRadius(size.height),
63 | bottomLeft = CornerRadius(size.height),
64 | )
65 | )
66 | EntryDrawShape.Middle -> addRect(rect = rect)
67 | EntryDrawShape.Last -> addRoundRect(
68 | RoundRect(
69 | rect = rect,
70 | topLeft = CornerRadius.Zero,
71 | bottomLeft = CornerRadius.Zero,
72 | topRight = CornerRadius(size.height),
73 | bottomRight = CornerRadius(size.height),
74 | )
75 | )
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/bars/data/HorizontalBarsData.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.bars.data
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import me.rerere.zhiwang.util.charts.legend.LegendEntry
5 |
6 | data class HorizontalBarsData(
7 | /**
8 | * List of horizontal bars to be drawn
9 | */
10 | val bars: List,
11 | /**
12 | * Optional
13 | *
14 | * Colors for every bar item
15 | */
16 | val colors: List = emptyList(),
17 | /**
18 | * Optional. Items specified here will replace items inferred from [bars]
19 | */
20 | val customLegendEntries: List = emptyList(),
21 | /**
22 | * Whether to enabled popup on bar click or not
23 | */
24 | val isPopupEnabled: Boolean = true,
25 | )
26 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/bars/data/StackedBarData.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.bars.data
2 |
3 | import androidx.compose.ui.text.AnnotatedString
4 |
5 | data class StackedBarData(
6 | val title: AnnotatedString,
7 | val entries: List,
8 | ) {
9 | val count get() = entries.sumBy { it.value.toInt() }
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/bars/data/StackedBarEntry.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.bars.data
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.text.AnnotatedString
5 |
6 | data class StackedBarEntry(
7 | val text: AnnotatedString,
8 | val value: Float,
9 | val color: Color? = null,
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/bars/data/StackedBarItem.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.bars.data
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.text.AnnotatedString
5 |
6 | internal data class StackedBarItem(
7 | val text: AnnotatedString,
8 | val value: Float,
9 | val color: Color,
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/internal/DefaultText.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.internal
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.text.AnnotatedString
6 |
7 | @Composable
8 | internal fun DefaultText(text: AnnotatedString?) {
9 | if (text != null)
10 | Text(text = text)
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/internal/Utils.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.internal
2 |
3 | internal fun List.safeGet(idx: Int): T = when {
4 | idx in 0..lastIndex -> this[idx]
5 | idx > lastIndex -> this[idx - size]
6 | else -> error("Can't get a color at $idx")
7 | }
8 |
9 | internal const val DEG2RAD = Math.PI / 180.0
10 | internal const val FDEG2RAD = Math.PI.toFloat() / 180f
11 | internal val FLOAT_EPSILON = Float.fromBits(1)
12 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/legend/HorizontalLegend.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.legend
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.requiredSize
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.unit.dp
12 | import com.google.accompanist.flowlayout.FlowRow
13 | import me.rerere.zhiwang.util.charts.internal.DefaultText
14 |
15 | @Composable
16 | fun DrawHorizontalLegend(
17 | legendEntries: List,
18 | text: @Composable (item: LegendEntry) -> Unit = { DefaultText(text = it.text) },
19 | ) {
20 | FlowRow(
21 | mainAxisSpacing = 16.dp,
22 | crossAxisSpacing = 8.dp,
23 | ) {
24 | legendEntries.forEachIndexed { _,item ->
25 | Row(verticalAlignment = Alignment.CenterVertically) {
26 | Box(
27 | modifier = Modifier
28 | .requiredSize(item.shape.size)
29 | .background(item.shape.color, item.shape.shape)
30 | )
31 |
32 | Spacer(modifier = Modifier.requiredSize(8.dp))
33 |
34 | text(item)
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/legend/LegendEntry.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.legend
2 |
3 | import androidx.compose.ui.text.AnnotatedString
4 | import me.rerere.zhiwang.util.charts.ChartShape
5 |
6 | data class LegendEntry(
7 | val text: AnnotatedString,
8 | val value: Float,
9 | val percent: Float = Float.MAX_VALUE,
10 | val shape: ChartShape = ChartShape.Default
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/legend/VerticalLegend.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.legend
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.unit.dp
9 | import me.rerere.zhiwang.util.charts.internal.DefaultText
10 |
11 | @Composable
12 | fun RowScope.DrawVerticalLegend(
13 | legendEntries: List,
14 | text: @Composable (entry: LegendEntry) -> Unit = {
15 | DefaultText(text = it.text)
16 | },
17 | ) {
18 | Column(
19 | modifier = Modifier.weight(1f),
20 | verticalArrangement = Arrangement.Center
21 | ) {
22 | legendEntries.forEachIndexed { idx, item ->
23 | Row(verticalAlignment = Alignment.CenterVertically) {
24 | Box(
25 | modifier = Modifier
26 | .requiredSize(item.shape.size)
27 | .background(item.shape.color, item.shape.shape)
28 | )
29 |
30 | Spacer(modifier = Modifier.requiredSize(8.dp))
31 |
32 | text(item)
33 | }
34 |
35 | if (idx != legendEntries.lastIndex)
36 | Spacer(modifier = Modifier.requiredSize(8.dp))
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/pie/LegendPosition.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.pie
2 |
3 | enum class LegendPosition {
4 | Start, End, Top, Bottom;
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/pie/PieChart.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.pie
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.platform.LocalDensity
9 | import androidx.compose.ui.unit.Dp
10 | import androidx.compose.ui.unit.dp
11 | import me.rerere.zhiwang.util.charts.legend.DrawVerticalLegend
12 | import me.rerere.zhiwang.util.charts.legend.LegendEntry
13 |
14 | @Composable
15 | fun PieChart(
16 | data: PieChartData,
17 | modifier: Modifier = Modifier,
18 | chartSize: Dp = 100.dp,
19 | sliceWidth: Dp = 16.dp,
20 | legendOffset: Dp = 24.dp,
21 | chartShapeSize: Dp = 8.dp,
22 | sliceSpacing: Dp = 2.dp,
23 | legend: @Composable (RowScope.(entries: List) -> Unit)? = null,
24 | ) {
25 | val fractions = remember(data) { data.calculateFractions() }
26 | val legendEntries = remember(data) { data.createLegendEntries(chartShapeSize) }
27 |
28 | val chartSizePx = with(LocalDensity.current) { chartSize.toPx() }
29 | val sliceWidthPx = with(LocalDensity.current) { sliceWidth.toPx() }
30 | val sliceSpacingPx = with(LocalDensity.current) { sliceSpacing.toPx() }
31 |
32 | @Composable
33 | fun RowScope.legend() {
34 | if (legend == null) {
35 | DrawVerticalLegend(legendEntries)
36 | } else {
37 | legend(legendEntries)
38 | }
39 | }
40 |
41 | Column(Modifier.fillMaxWidth()) {
42 | if (data.legendPosition == LegendPosition.Top) {
43 | Row {
44 | legend()
45 | }
46 | Spacer(modifier = Modifier.requiredSize(legendOffset))
47 | }
48 |
49 | Row(
50 | modifier = modifier.fillMaxWidth(),
51 | verticalAlignment = Alignment.CenterVertically,
52 | horizontalArrangement = Arrangement.Center
53 | ) {
54 | val entryColors = data.entries.mapNotNull { it.color }
55 |
56 | if (data.legendPosition == LegendPosition.Start) {
57 | legend()
58 | Spacer(modifier = Modifier.requiredSize(legendOffset))
59 | }
60 |
61 | PieChartRenderer(
62 | modifier = Modifier.requiredSize(chartSize),
63 | chartSizePx = chartSizePx,
64 | sliceWidthPx = sliceWidthPx,
65 | sliceSpacingPx = sliceSpacingPx,
66 | fractions = fractions,
67 | composeColors = entryColors.takeIf { it.size == data.entries.size } ?: data.colors,
68 | animate = data.animate,
69 | )
70 |
71 | if (data.legendPosition == LegendPosition.End) {
72 | Spacer(modifier = Modifier.requiredSize(legendOffset))
73 | legend()
74 | }
75 | }
76 |
77 | if (data.legendPosition == LegendPosition.Bottom) {
78 | Spacer(modifier = Modifier.requiredSize(legendOffset))
79 | Row {
80 | legend()
81 | }
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/pie/PieChartData.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.pie
2 |
3 | import androidx.compose.foundation.shape.CircleShape
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.compose.ui.graphics.Shape
6 |
7 | data class PieChartData(
8 | val entries: List,
9 | val colors: List = emptyList(),
10 | val legendPosition: LegendPosition = LegendPosition.Bottom,
11 | val legendShape: Shape = CircleShape,
12 | val animate: Boolean = true,
13 | )
14 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/pie/PieChartEntry.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.pie
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.text.AnnotatedString
5 |
6 | data class PieChartEntry(
7 | val value: Float,
8 | val label: AnnotatedString,
9 | /**
10 | * Color of the pie slice and legend entry, if not provided [PieChartData.colors] will be used
11 | */
12 | val color: Color? = null,
13 | )
14 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/pie/PieChartRenderer.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.pie
2 |
3 | import android.graphics.Paint
4 | import android.graphics.Path
5 | import android.graphics.RectF
6 | import androidx.compose.animation.core.Animatable
7 | import androidx.compose.animation.core.tween
8 | import androidx.compose.foundation.Canvas
9 | import androidx.compose.runtime.*
10 | import androidx.compose.runtime.saveable.rememberSaveable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.graphics.nativeCanvas
14 | import androidx.compose.ui.graphics.toArgb
15 | import me.rerere.zhiwang.util.charts.internal.FDEG2RAD
16 | import me.rerere.zhiwang.util.charts.internal.FLOAT_EPSILON
17 | import me.rerere.zhiwang.util.charts.internal.safeGet
18 | import kotlin.math.cos
19 | import kotlin.math.max
20 | import kotlin.math.sin
21 |
22 | private const val StartDegree = -90f
23 |
24 | @Composable
25 | internal fun PieChartRenderer(
26 | modifier: Modifier = Modifier,
27 | chartSizePx: Float,
28 | sliceWidthPx: Float,
29 | sliceSpacingPx: Float,
30 | fractions: List,
31 | composeColors: List,
32 | animate: Boolean,
33 | ) {
34 | var animationRan by rememberSaveable(fractions, animate) { mutableStateOf(false) }
35 | val animation = remember {
36 | Animatable(
37 | when {
38 | animate -> if (animationRan) 1f else 0f
39 | else -> 1f
40 | }
41 | )
42 | }
43 | val phase by animation.asState()
44 |
45 | LaunchedEffect(Unit) {
46 | if (!animationRan)
47 | animation.animateTo(1f, tween(325)) { animationRan = true }
48 | }
49 |
50 | val pathBuffer by remember { mutableStateOf(Path()) }
51 | val innerRectBuffer by remember { mutableStateOf(RectF()) }
52 |
53 | val holeRadius = remember(chartSizePx, sliceWidthPx) {
54 | (chartSizePx - (sliceWidthPx * 2f)) / 2f
55 | }
56 |
57 | Canvas(modifier = modifier) {
58 | val circleBox = RectF(0f, 0f, size.width, size.height)
59 | val nativeCanvas = drawContext.canvas.nativeCanvas
60 |
61 | var angle = 0f
62 |
63 | val radius = chartSizePx / 2f
64 | val drawInnerArc = sliceWidthPx > FLOAT_EPSILON && sliceWidthPx < chartSizePx / 2f
65 | val userInnerRadius = if (drawInnerArc) holeRadius else 0f
66 |
67 | val rotationAngle = StartDegree
68 |
69 | fractions.forEachIndexed { idx, sliceAngle ->
70 | val piecePaint = Paint().apply {
71 | color = composeColors.safeGet(idx).toArgb()
72 | isAntiAlias = true
73 | }
74 | var innerRadius = userInnerRadius
75 |
76 | val accountForSliceSpacing = sliceSpacingPx > 0f && sliceAngle <= 180f
77 |
78 | val sliceSpaceAngleOuter = sliceSpacingPx / (FDEG2RAD * radius)
79 | val startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2f) * phase
80 | val sweepAngleOuter = ((sliceAngle - sliceSpaceAngleOuter) * phase).coerceAtLeast(0f)
81 |
82 | pathBuffer.reset()
83 |
84 | val arcStartPointX = center.x + radius * cos(startAngleOuter * FDEG2RAD)
85 | val arcStartPointY = center.y + radius * sin(startAngleOuter * FDEG2RAD)
86 |
87 | if (sweepAngleOuter >= 360f && sweepAngleOuter % 360f <= FLOAT_EPSILON) {
88 | // Android is doing "mod 360"
89 | pathBuffer.addCircle(center.x, center.y, radius, Path.Direction.CW)
90 | } else {
91 | pathBuffer.arcTo(
92 | circleBox,
93 | startAngleOuter,
94 | sweepAngleOuter
95 | )
96 | }
97 |
98 | innerRectBuffer.set(
99 | center.x - innerRadius,
100 | center.y - innerRadius,
101 | center.x + innerRadius,
102 | center.y + innerRadius
103 | )
104 |
105 | if (drawInnerArc && (innerRadius > 0f || accountForSliceSpacing)) {
106 | if (accountForSliceSpacing) {
107 | val minSpacedRadius = calculateMinimumRadiusForSpacedSlice(
108 | center,
109 | radius,
110 | sliceAngle * phase,
111 | arcStartPointX, arcStartPointY,
112 | startAngleOuter,
113 | sweepAngleOuter
114 | ).let {
115 | if (it < 0f) -it else it
116 | }
117 |
118 | innerRadius = max(innerRadius, minSpacedRadius)
119 | }
120 |
121 | val sliceSpaceAngleInner = if (fractions.size == 1 || innerRadius == 0f) 0f
122 | else sliceSpacingPx / (FDEG2RAD * innerRadius)
123 |
124 | val startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2f) * phase
125 | val sweepAngleInner = ((sliceAngle - sliceSpaceAngleInner) * phase).coerceAtLeast(0f)
126 |
127 | val endAngleInner = startAngleInner + sweepAngleInner
128 | if (sweepAngleOuter >= 360f && sweepAngleOuter % 360f <= FLOAT_EPSILON) {
129 | // Android is doing "mod 360"
130 | pathBuffer.addCircle(center.x, center.y, innerRadius, Path.Direction.CCW)
131 | } else {
132 | pathBuffer.lineTo(
133 | center.x + innerRadius * cos(endAngleInner * FDEG2RAD),
134 | center.y + innerRadius * sin(endAngleInner * FDEG2RAD)
135 | )
136 |
137 | pathBuffer.arcTo(
138 | innerRectBuffer,
139 | endAngleInner,
140 | -sweepAngleInner
141 | )
142 | }
143 | } else {
144 | if (sweepAngleOuter % 360f > FLOAT_EPSILON) {
145 | if (accountForSliceSpacing) {
146 | val angleMiddle = startAngleOuter + sweepAngleOuter / 2f
147 | val sliceSpaceOffset = calculateMinimumRadiusForSpacedSlice(
148 | center,
149 | radius,
150 | sliceAngle * phase,
151 | arcStartPointX,
152 | arcStartPointY,
153 | startAngleOuter,
154 | sweepAngleOuter
155 | )
156 | val arcEndPointX = center.x + sliceSpaceOffset * cos(angleMiddle * FDEG2RAD)
157 | val arcEndPointY = center.y + sliceSpaceOffset * sin(angleMiddle * FDEG2RAD)
158 | pathBuffer.lineTo(
159 | arcEndPointX,
160 | arcEndPointY
161 | )
162 | } else {
163 | pathBuffer.lineTo(
164 | center.x,
165 | center.y
166 | )
167 | }
168 | }
169 | }
170 |
171 | pathBuffer.close()
172 |
173 | nativeCanvas.drawPath(pathBuffer, piecePaint)
174 |
175 | angle += sliceAngle * phase
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/pie/Utils.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.pie
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.unit.Dp
5 | import me.rerere.zhiwang.util.charts.ChartShape
6 | import me.rerere.zhiwang.util.charts.internal.DEG2RAD
7 | import me.rerere.zhiwang.util.charts.internal.FDEG2RAD
8 | import me.rerere.zhiwang.util.charts.internal.safeGet
9 | import me.rerere.zhiwang.util.charts.legend.LegendEntry
10 | import kotlin.math.*
11 |
12 | internal fun PieChartData.createLegendEntries(
13 | shapeSize: Dp,
14 | ): List =
15 | entries.mapIndexed { index, item ->
16 | LegendEntry(
17 | text = item.label,
18 | value = item.value,
19 | percent = item.value * 100f / entries.map { it.value }.reduce { acc, i -> acc + i },
20 | shape = ChartShape(
21 | color = item.color ?: colors.safeGet(index),
22 | shape = legendShape,
23 | size = shapeSize,
24 | )
25 | )
26 | }
27 |
28 | internal fun PieChartData.calculateFractions(
29 | minAngle: Float = 16f,
30 | maxAngle: Float = 360f
31 | ): List {
32 | val total = entries.sumByDouble { it.value.toDouble() }.toFloat()
33 | val entryCount = entries.size
34 |
35 | val hasMinAngle = minAngle != 0f && entryCount * minAngle <= maxAngle
36 | val minAngles = MutableList(entryCount) { 0f }
37 |
38 | val fractions = entries
39 | .map { it.value / total }
40 | .map { it * 360f }
41 |
42 | var offset = 0f
43 | var diff = 0f
44 |
45 | if (hasMinAngle) {
46 | fractions.forEachIndexed { idx, angle ->
47 | val temp = angle - minAngle
48 |
49 | if (temp <= 0) {
50 | offset += -temp
51 | minAngles[idx] = minAngle
52 | } else {
53 | minAngles[idx] = angle
54 | diff += temp
55 | }
56 | }
57 |
58 | fractions.forEachIndexed { idx, _ ->
59 | minAngles[idx] -= (minAngles[idx] - minAngle) / diff * offset
60 | }
61 |
62 | return minAngles
63 | }
64 |
65 | return fractions
66 | }
67 |
68 | internal fun calculateMinimumRadiusForSpacedSlice(
69 | center: Offset,
70 | radius: Float,
71 | angle: Float,
72 | arcStartPointX: Float,
73 | arcStartPointY: Float,
74 | startAngle: Float,
75 | sweepAngle: Float
76 | ): Float {
77 | val angleMiddle = startAngle + sweepAngle / 2f
78 |
79 | // Other point of the arc
80 | val arcEndPointX: Float = center.x + radius * cos((startAngle + sweepAngle) * FDEG2RAD)
81 | val arcEndPointY: Float = center.y + radius * sin((startAngle + sweepAngle) * FDEG2RAD)
82 |
83 | // Middle point on the arc
84 | val arcMidPointX: Float = center.x + radius * cos(angleMiddle * FDEG2RAD)
85 | val arcMidPointY: Float = center.y + radius * sin(angleMiddle * FDEG2RAD)
86 |
87 | // This is the base of the contained triangle
88 | val basePointsDistance = sqrt(
89 | (arcEndPointX - arcStartPointX).toDouble().pow(2.0) +
90 | (arcEndPointY - arcStartPointY).toDouble().pow(2.0)
91 | )
92 |
93 | // After reducing space from both sides of the "slice",
94 | // the angle of the contained triangle should stay the same.
95 | // So let's find out the height of that triangle.
96 | val containedTriangleHeight =
97 | (basePointsDistance / 2.0 * tan((180.0 - angle) / 2.0 * DEG2RAD)).toFloat()
98 |
99 | // Now we subtract that from the radius
100 | var spacedRadius = radius - containedTriangleHeight
101 |
102 | // And now subtract the height of the arc that's between the triangle and the outer circle
103 | spacedRadius -= sqrt(
104 | (arcMidPointX - (arcEndPointX + arcStartPointX) / 2f).toDouble().pow(2.0) +
105 | (arcMidPointY - (arcEndPointY + arcStartPointY) / 2f).toDouble().pow(2.0)
106 | ).toFloat()
107 |
108 | return spacedRadius
109 | }
110 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/table/Table.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.table
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.RowScope
5 | import androidx.compose.foundation.layout.requiredSize
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.text.AnnotatedString
9 | import androidx.compose.ui.unit.dp
10 | import me.rerere.zhiwang.util.charts.internal.DefaultText
11 |
12 | @Deprecated(
13 | "Use TableChart instead",
14 | ReplaceWith("TableChart(data, modifier, shapeModifier, keyText, valueText, divider)")
15 | )
16 | @Composable
17 | fun Table(
18 | data: List,
19 | modifier: Modifier = Modifier,
20 | shapeModifier: Modifier = Modifier.requiredSize(8.dp),
21 | keyText: @Composable RowScope.(key: AnnotatedString?) -> Unit = { DefaultText(text = it) },
22 | valueText: @Composable RowScope.(value: AnnotatedString?) -> Unit = { DefaultText(text = it) },
23 | divider: @Composable (() -> Unit)? = null,
24 | ) {
25 | TableChart(data, modifier, shapeModifier, keyText, valueText, divider)
26 | }
27 |
28 | @Composable
29 | fun TableChart(
30 | data: List,
31 | modifier: Modifier = Modifier,
32 | shapeModifier: Modifier = Modifier.requiredSize(8.dp),
33 | keyText: @Composable RowScope.(key: AnnotatedString?) -> Unit = { DefaultText(text = it) },
34 | valueText: @Composable RowScope.(value: AnnotatedString?) -> Unit = { DefaultText(text = it) },
35 | divider: @Composable (() -> Unit)? = null,
36 | ) {
37 | Column(modifier) {
38 | data.forEachIndexed { idx, item ->
39 | TableRow(
40 | entry = item,
41 | shapeModifier = shapeModifier,
42 | keyText = keyText,
43 | valueText = valueText,
44 | )
45 | if (idx != data.lastIndex && divider != null)
46 | divider()
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/table/TableEntry.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.table
2 |
3 | import androidx.compose.ui.text.AnnotatedString
4 | import me.rerere.zhiwang.util.charts.ChartShape
5 |
6 | data class TableEntry(
7 | val key: AnnotatedString?,
8 | val value: AnnotatedString?,
9 | val drawShape: ChartShape? = null,
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/charts/table/TableRow.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.charts.table
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.text.AnnotatedString
9 |
10 | @Composable
11 | internal fun TableRow(
12 | modifier: Modifier = Modifier,
13 | entry: TableEntry,
14 | shapeModifier: Modifier,
15 | keyText: @Composable RowScope.(key: AnnotatedString?) -> Unit,
16 | valueText: @Composable RowScope.(value: AnnotatedString?) -> Unit,
17 | ) {
18 | Row(
19 | modifier = modifier,
20 | verticalAlignment = Alignment.CenterVertically
21 | ) {
22 | if (entry.drawShape != null) {
23 | Box(
24 | modifier = shapeModifier.background(entry.drawShape.color, entry.drawShape.shape)
25 | )
26 | Spacer(modifier = Modifier.requiredSize(entry.drawShape.size))
27 | }
28 |
29 | keyText(entry.key)
30 |
31 | Spacer(modifier = Modifier.weight(1f))
32 |
33 | valueText(entry.value)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/format/Format.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.format
2 |
3 | import kotlin.math.roundToInt
4 |
5 | // format double
6 | fun Double.format() = (this * 1000).roundToInt() / 1000.0
7 | fun Double.formatToString() = format().toString()
8 |
9 | // format float
10 | fun Float.format() = (this * 1000).roundToInt() / 1000.0
11 | fun Float.formatToString() = format().toString()
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/format/HtmlFormatUtil.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.format
2 |
3 | import org.jsoup.internal.StringUtil
4 | import org.jsoup.nodes.Element
5 | import org.jsoup.nodes.Node
6 | import org.jsoup.nodes.TextNode
7 | import org.jsoup.select.NodeTraversor
8 | import org.jsoup.select.NodeVisitor
9 |
10 | object HtmlFormatUtil {
11 | fun getPlainText(element: Element?): String? {
12 | val formatter = FormattingVisitor()
13 | NodeTraversor.traverse(
14 | formatter,
15 | element
16 | ) // walk the DOM, and call .head() and .tail() for each node
17 | return formatter.toString()
18 | }
19 |
20 | // the formatting rules, implemented in a breadth-first DOM traverse
21 | private class FormattingVisitor : NodeVisitor {
22 | private var width = 0
23 | private val accum = StringBuilder() // holds the accumulated text
24 |
25 | // hit when the node is first seen
26 | override fun head(node: Node, depth: Int) {
27 | val name: String = node.nodeName()
28 | if (node is TextNode) append((node as TextNode).text()) // TextNodes carry all user-readable text in the DOM.
29 | else if (name == "li") append("\n * ") else if (name == "dt") append(" ") else if (StringUtil.`in`(
30 | name,
31 | "p",
32 | "h1",
33 | "h2",
34 | "h3",
35 | "h4",
36 | "h5",
37 | "tr"
38 | )
39 | ) append("\n")
40 | }
41 |
42 | // hit when all of the node's children (if any) have been visited
43 | override fun tail(node: Node, depth: Int) {
44 | val name: String = node.nodeName()
45 | if (StringUtil.`in`(
46 | name,
47 | "br",
48 | "dd",
49 | "dt",
50 | "p",
51 | "h1",
52 | "h2",
53 | "h3",
54 | "h4",
55 | "h5"
56 | )
57 | ) append("\n") else if (name == "a") append(
58 | java.lang.String.format(
59 | " <%s>",
60 | node.absUrl("href")
61 | )
62 | )
63 | }
64 |
65 | // appends text to the string builder with a simple word wrap method
66 | private fun append(text: String) {
67 | if (text.startsWith("\n")) width =
68 | 0 // reset counter if starts with a newline. only from formats above, not in natural text
69 | if (text == " " &&
70 | (accum.length == 0 || StringUtil.`in`(accum.substring(accum.length - 1), " ", "\n"))
71 | ) return // don't accumulate long runs of empty spaces
72 | if (text.length + width > maxWidth) { // won't fit, needs to wrap
73 | val words = text.split("\\s+").toTypedArray()
74 | for (i in words.indices) {
75 | var word = words[i]
76 | val last = i == words.size - 1
77 | if (!last) // insert a space if not the last word
78 | word = "$word "
79 | if (word.length + width > maxWidth) { // wrap and reset counter
80 | accum.append("\n").append(word)
81 | width = word.length
82 | } else {
83 | accum.append(word)
84 | width += word.length
85 | }
86 | }
87 | } else { // fits as is, without need to wrap text
88 | accum.append(text)
89 | width += text.length
90 | }
91 | }
92 |
93 | override fun toString(): String {
94 | return accum.toString()
95 | }
96 |
97 | companion object {
98 | private const val maxWidth = 80
99 | }
100 | }
101 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/net/AutoRetry.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.net
2 |
3 | import android.util.Log
4 | import androidx.annotation.IntRange
5 | import me.rerere.zhiwang.api.zhiwang.Response
6 |
7 | private const val TAG = "AutoRetry"
8 |
9 | /**
10 | * 自动重试函数
11 | *
12 | * @param maxRetry 重试次数
13 | * @param action 重试体
14 | * @return 最终响应
15 | */
16 | suspend fun autoRetry(
17 | @IntRange(from = 2) maxRetry: Int = 3, // 重连次数
18 | action: suspend () -> Response
19 | ): Response? {
20 | repeat(maxRetry - 1) {
21 | Log.i(TAG, "autoRetry: Try to get response: ${it + 1}/$maxRetry")
22 | val start = System.currentTimeMillis()
23 | val response = try {
24 | action()
25 | } catch (e: Exception) {
26 | e.printStackTrace()
27 | null
28 | }
29 | if (response != null) {
30 | Log.i(
31 | TAG,
32 | "autoRetry: Successful get response (${System.currentTimeMillis() - start} ms)"
33 | )
34 | return response
35 | }
36 | }
37 | Log.i(TAG, "autoRetry: Try to get response: $maxRetry*/$maxRetry")
38 | return try { action() } catch (e: Exception){
39 | e.printStackTrace()
40 | null
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/net/OkhttpUtil.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.net
2 |
3 | import kotlinx.coroutines.suspendCancellableCoroutine
4 | import okhttp3.Call
5 | import okhttp3.Callback
6 | import okhttp3.Response
7 | import java.io.IOException
8 | import kotlin.coroutines.resume
9 | import kotlin.coroutines.resumeWithException
10 |
11 | suspend fun Call.await(): Response {
12 | return suspendCancellableCoroutine {
13 | enqueue(object : Callback {
14 | override fun onFailure(call: Call, e: IOException) {
15 | if(it.isCancelled) return
16 | it.resumeWithException(e)
17 | }
18 |
19 | override fun onResponse(call: Call, response: Response) {
20 | it.resume(response)
21 | }
22 | })
23 |
24 | it.invokeOnCancellation {
25 | try {
26 | cancel()
27 | } catch (e: Exception){
28 | println("===== CANCEL ======")
29 | // IGNORE
30 | }
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/rerere/zhiwang/util/net/UserAgentInterceptor.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang.util.net
2 |
3 | import okhttp3.Interceptor
4 | import okhttp3.Request
5 | import okhttp3.Response
6 |
7 | class UserAgentInterceptor(private val userAgent: String) : Interceptor {
8 | override fun intercept(chain: Interceptor.Chain): Response {
9 | val userAgentRequest: Request = chain.request()
10 | .newBuilder()
11 | .header("User-Agent", userAgent)
12 | .build()
13 | return chain.proceed(userAgentRequest)
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/asoul.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/app/src/main/res/drawable-v24/asoul.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/chengfen.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/app/src/main/res/mipmap-hdpi/ic_launcher_rounded.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/app/src/main/res/mipmap-mdpi/ic_launcher_rounded.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/app/src/main/res/mipmap-xhdpi/ic_launcher_rounded.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/app/src/main/res/mipmap-xxhdpi/ic_launcher_rounded.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/app/src/main/res/mipmap-xxxhdpi/ic_launcher_rounded.png
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | 小作文助手
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/test/java/me/rerere/zhiwang/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package me.rerere.zhiwang
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 | dependencies {
8 | classpath ("com.android.tools.build:gradle:7.1.0-beta02")
9 | classpath ("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")
10 | classpath ("com.google.dagger:hilt-android-gradle-plugin:$hiltVersion")
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | task("clean") {
18 | delete (rootProject.buildDir)
19 | }
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | google()
7 | mavenCentral()
8 | jcenter()
9 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/Versions.kt:
--------------------------------------------------------------------------------
1 | // JetPack Compose 版本
2 | const val composeVersion = "1.1.0-beta01"
3 |
4 | // Hilt 版本
5 | const val hiltVersion = "2.39.1"
6 |
7 | // accompanist 版本
8 | // https://github.com/google/accompanist
9 | const val accVersion = "0.21.0-beta"
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jul 06 01:09:58 CST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/screenshots/chengfen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/screenshots/chengfen.png
--------------------------------------------------------------------------------
/screenshots/query.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/screenshots/query.png
--------------------------------------------------------------------------------
/screenshots/zuowen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-ovo/ASoulZhiWang/a39289b0bfe9a56c591869517fbb90cf5df968cf/screenshots/zuowen.png
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | jcenter() // Warning: this repository is going to shut down soon
7 | }
8 | }
9 | rootProject.name = "ZhiWang"
10 | include (":app")
11 |
--------------------------------------------------------------------------------
/wiki.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "小作文",
4 | "description": "早期嘉然评论区下的zqsg,发病作品等兴起的文化,可以说是一大特色,后面扩散到了整个asoul成员的评论区里"
5 | },
6 | {
7 | "title": "萌萌人 / MMR",
8 | "description": "全肯定所有自己推的偶像vtb观点"
9 | },
10 | {
11 | "title": "乐子人",
12 | "description": "以寻求乐子为主,观看v的观众"
13 | },
14 | {
15 | "title": "老鼠人",
16 | "description": "鼠人,即老鼠人,特指下水道的老鼠人。 网络丧文化将社会底层的群体,比喻成老鼠人,网上流传了这样一句话“我是下水道的老鼠,也想抬头看看天”。 "
17 | },
18 | {
19 | "title": "要抱抱 / ybb",
20 | "description": "有病吧"
21 | },
22 | {
23 | "title": "逆天",
24 | "description": "表示佩服 (带有微微的贬义)"
25 | },
26 | {
27 | "title": "收到收到收到",
28 | "description": "一位奶琪琳的十分厉害(逆天)的作品中出现"
29 | },
30 | {
31 | "title": "啊笑死",
32 | "description": "嘉然的口头禅"
33 | },
34 | {
35 | "title": "\uD83C\uDFA4",
36 | "description": "麦克风,出自“谈谈理想型”,珈乐的画被超版误认为是(寄吧)导致封直播间"
37 | },
38 | {
39 | "title": "风情",
40 | "description": "出处是这个词出现前弹幕对于舞蹈的回应是烧(谐音sao),有次珈乐单播表演完后说对女孩子不能这样说,应该说风情,然后沿用至今"
41 | },
42 | {
43 | "title": "\uD83D\uDE05",
44 | "description": "\uD83D\uDE05"
45 | },
46 | {
47 | "title": "嗨呀是我吗",
48 | "description": "嗨呀是我吗这个梗是嘉然一个指着自己的表情包最先在a吧里流传,然后弹幕在直播讨论理想型时大量刷屏,然后届到了吧。后面在“谈谈理想型”团播,成员们互相认定自己是对方的理想型。"
49 | },
50 | {
51 | "title": "大聪明",
52 | "description": "贝拉的爱称"
53 | },
54 | {
55 | "title": "\uD83E\uDD16",
56 | "description": "贝拉在一次直播时,由于输入问题导致自己的声音变成了电音,于是有了robot的美称。"
57 | },
58 | {
59 | "title": "圣嘉然",
60 | "description": "zj梗,嘉然的第一批粉丝本来是来取乐的,但是嘉然用实力和真情实感感化了观众。相关二创作品 https://b23.tv/kUhZFn"
61 | },
62 | {
63 | "title": "嘉然小姐的狗",
64 | "description": "新户眠子所写小作文,因其极强的现实讽刺性,被广为传颂,涌现出大量以此为蓝本的二创(都不能转)。(在3.7嘉然生日会时,向晚朗诵改编版“嘉然小姐的碗”,间接代表了官方对此类二创的认可。)"
65 | },
66 | {
67 | "title": "8u",
68 | "description": "A-soul贴吧吧友;V吧贴吧吧友"
69 | },
70 | {
71 | "title": "梁木",
72 | "description": "对一个人有很大的成见"
73 | },
74 | {
75 | "title": "困惑害怕不安",
76 | "description": "著名vtb kaguranana女士在面对中文弹幕时说到,困惑害怕不安,请不要再发了。(事实上是翻译错误)"
77 | },
78 | {
79 | "title": "yhm",
80 | "description": "著樱花妹缩写,广义上指日本女生,狭义上指日本的vtuber"
81 | },
82 | {
83 | "title": "你懂不懂xx的含金量啊",
84 | "description": "炫神直播吹嘘自己曾经的成绩,由于过于有节目效果而流传成梗"
85 | },
86 | {
87 | "title": "溜大了",
88 | "description": "本意是指xd,冰就是b粉,因山泥若直播时经常脸色苍白,而被粉丝调侃为瘾君子喜欢溜冰,后解构为对一切事物的成瘾性迷恋。溜大了即指失去理智之后所做的行为"
89 | },
90 | {
91 | "title": "铸币",
92 | "description": "“大伙儿都挺猛的,怎么到你这就这么拉胯了”,均语出电棍"
93 | },
94 | {
95 | "title": "含金量",
96 | "description": "炫神直播吹嘘自己曾经的成绩,由于过于有节目效果而流传成梗"
97 | },
98 | {
99 | "title": "你寄吧谁啊",
100 | "description": "百度贴吧常见的话,谐音梗"
101 | }
102 | ]
--------------------------------------------------------------------------------