├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── anpe │ │ └── coolbbsyou │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── anpe │ │ │ └── coolbbsyou │ │ │ ├── constant │ │ │ └── Constants.kt │ │ │ ├── network │ │ │ ├── data │ │ │ │ ├── cookie │ │ │ │ │ ├── CookieManger.kt │ │ │ │ │ ├── OkHttpCookies.kt │ │ │ │ │ └── PersistentCookieStore.kt │ │ │ │ ├── intent │ │ │ │ │ └── MainIntent.kt │ │ │ │ ├── model │ │ │ │ │ ├── details │ │ │ │ │ │ └── DetailsEntity.kt │ │ │ │ │ ├── deviceInfo │ │ │ │ │ │ └── DeviceInfo.kt │ │ │ │ │ ├── index │ │ │ │ │ │ └── IndexEntity.kt │ │ │ │ │ ├── login │ │ │ │ │ │ └── LoginEntity.kt │ │ │ │ │ ├── loginState │ │ │ │ │ │ └── LoginStateEntity.kt │ │ │ │ │ ├── nofitication │ │ │ │ │ │ └── NotificationEntity.kt │ │ │ │ │ ├── profile │ │ │ │ │ │ └── ProfileEntity.kt │ │ │ │ │ ├── reply │ │ │ │ │ │ └── ReplyEntity.kt │ │ │ │ │ ├── suggest │ │ │ │ │ │ └── SuggestSearchEntity.kt │ │ │ │ │ └── today │ │ │ │ │ │ └── TodayCoolEntity.kt │ │ │ │ ├── repository │ │ │ │ │ └── ApiRepository.kt │ │ │ │ ├── source │ │ │ │ │ ├── IndexSource.kt │ │ │ │ │ ├── NotificationSource.kt │ │ │ │ │ └── ReplySource.kt │ │ │ │ └── state │ │ │ │ │ ├── DetailsState.kt │ │ │ │ │ ├── IndexImageState.kt │ │ │ │ │ ├── IndexState.kt │ │ │ │ │ ├── LoginInfoState.kt │ │ │ │ │ ├── LoginState.kt │ │ │ │ │ ├── NotificationState.kt │ │ │ │ │ ├── ProfileState.kt │ │ │ │ │ ├── ReplyState.kt │ │ │ │ │ ├── SuggestState.kt │ │ │ │ │ └── TodayState.kt │ │ │ └── service │ │ │ │ ├── ApiService.kt │ │ │ │ └── ApiServiceTwo.kt │ │ │ ├── ui │ │ │ ├── innerScreen │ │ │ │ ├── TodaySelectionScreen.kt │ │ │ │ └── manager │ │ │ │ │ └── InnerScreenManager.kt │ │ │ ├── main │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainViewModel.kt │ │ │ ├── pager │ │ │ │ ├── DetailsPager.kt │ │ │ │ ├── HomePager.kt │ │ │ │ ├── MessagePager.kt │ │ │ │ ├── SettingsPager.kt │ │ │ │ ├── TodayCoolPager.kt │ │ │ │ └── manager │ │ │ │ │ └── PagerManager.kt │ │ │ ├── screen │ │ │ │ ├── DetailsScreen.kt │ │ │ │ ├── LoginScreen.kt │ │ │ │ ├── MainScreen.kt │ │ │ │ ├── SplashScreen.kt │ │ │ │ └── manager │ │ │ │ │ └── ScreenManager.kt │ │ │ ├── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── view │ │ │ │ ├── BottomSheetDialog.kt │ │ │ │ ├── CustomProgress.kt │ │ │ │ ├── DetailsPagerBridge.kt │ │ │ │ ├── FullScreenImage.kt │ │ │ │ ├── LoadableLazyColumn.kt │ │ │ │ ├── MyDialog.kt │ │ │ │ ├── MyLabel.kt │ │ │ │ ├── MyScaffolWithDetails.kt │ │ │ │ ├── MyScaffold.kt │ │ │ │ ├── MySheetScaffold.kt │ │ │ │ ├── MyTest.kt │ │ │ │ ├── NineImageGrid.kt │ │ │ │ ├── ResponsiveLayout.kt │ │ │ │ └── TextIcon.kt │ │ │ └── util │ │ │ ├── LoginUtils.kt │ │ │ ├── MyApplication.kt │ │ │ ├── SharedPreferencesUtils.kt │ │ │ ├── ToastUtils.kt │ │ │ ├── TokenDeviceUtils.kt │ │ │ └── Utils.kt │ └── res │ │ ├── drawable │ │ ├── baseline_chat_bubble_24.xml │ │ ├── baseline_cloud_24.xml │ │ ├── baseline_error_24.xml │ │ ├── baseline_exit_to_app_24.xml │ │ ├── baseline_hdr_strong_24.xml │ │ ├── baseline_history_24.xml │ │ ├── baseline_home_24.xml │ │ ├── baseline_label_24.xml │ │ ├── baseline_message_24.xml │ │ ├── baseline_reply_24.xml │ │ ├── baseline_settings_24.xml │ │ ├── baseline_share_24.xml │ │ ├── baseline_star_24.xml │ │ ├── baseline_supervised_user_circle_24.xml │ │ ├── baseline_thumb_up_alt_24.xml │ │ ├── coolapk.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ └── ic_user_avatar.xml │ │ ├── mipmap-anydpi │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-en │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── com │ └── anpe │ └── coolbbsyou │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 125 | 126 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # *CoolbbsYou* 2 | 项目使用了Jetpack Compose Ui框架,MVI架构编写的第三方酷安。 3 | 4 | ## 声明 5 | 1. 本项目属于个人学习项目,仅用于学习和测试,切勿滥用,切勿滥用,切勿滥用。 6 | 2. 使用本项目中造成的不良影响及后果与本人无关。 7 | 8 | ## 开发节奏? 9 | - [X] 登录 10 | - [ ] 个人主页(已实现主页信息展示) 11 | - [ ] 首页(已实现信息流展示,操作性功能未实现) 12 | - [ ] 搜索(已实现搜索建议,其余未实现) 13 | - [ ] 详情页(已实现基础信息展示,UI在调整) 14 | - [ ] 消息页(已实现信息流展示, 未实现跳转) 15 | - [ ] 每日酷安(已实现信息流展示,未实现详情页) 16 | - [ ] 评论区 (已实现基础评论展示,未实现楼中楼) 17 | - [ ] 话题页 18 | - [ ] 关注页 19 | - [ ] Tab标签 20 | - [ ] 发帖功能 21 | - [ ] ...... 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed 2 | plugins { 3 | alias(libs.plugins.androidApplication) 4 | alias(libs.plugins.kotlinAndroid) 5 | } 6 | 7 | android { 8 | namespace = "com.anpe.coolbbsyou" 9 | compileSdk = 33 10 | 11 | defaultConfig { 12 | applicationId = "com.anpe.coolbbsyou" 13 | minSdk = 26 14 | targetSdk = 33 15 | versionCode = 1 16 | versionName = "0.23.0726" 17 | 18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 19 | vectorDrawables { 20 | useSupportLibrary = true 21 | } 22 | } 23 | 24 | buildTypes { 25 | release { 26 | isMinifyEnabled = false 27 | proguardFiles( 28 | getDefaultProguardFile("proguard-android-optimize.txt"), 29 | "proguard-rules.pro" 30 | ) 31 | } 32 | } 33 | compileOptions { 34 | sourceCompatibility = JavaVersion.VERSION_1_8 35 | targetCompatibility = JavaVersion.VERSION_1_8 36 | } 37 | kotlinOptions { 38 | jvmTarget = "1.8" 39 | } 40 | buildFeatures { 41 | compose = true 42 | } 43 | composeOptions { 44 | kotlinCompilerExtensionVersion = "1.4.3" 45 | } 46 | packaging { 47 | resources { 48 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 49 | } 50 | } 51 | } 52 | 53 | dependencies { 54 | implementation(libs.core.ktx) 55 | implementation(libs.lifecycle.runtime.ktx) 56 | implementation(libs.activity.compose) 57 | implementation(platform(libs.compose.bom)) 58 | implementation(libs.ui) 59 | implementation(libs.ui.graphics) 60 | implementation(libs.ui.tooling.preview) 61 | implementation(libs.material3) 62 | testImplementation(libs.junit) 63 | androidTestImplementation(libs.androidx.test.ext.junit) 64 | androidTestImplementation(libs.espresso.core) 65 | androidTestImplementation(platform(libs.compose.bom)) 66 | androidTestImplementation(libs.ui.test.junit4) 67 | debugImplementation(libs.ui.tooling) 68 | debugImplementation(libs.ui.test.manifest) 69 | 70 | // User library 71 | implementation(libs.constraintlayout.compose) 72 | implementation(libs.navigation.compose) 73 | implementation(libs.accompanist.swiperefresh) 74 | implementation(libs.accompanist.systemuicontroller) 75 | implementation(libs.gson) 76 | implementation(libs.coil.compose) 77 | implementation(libs.retrofit) 78 | implementation(libs.converter.gson) 79 | implementation(libs.richtext.ui.material3) 80 | implementation(libs.material) 81 | implementation(libs.paging.runtime) 82 | implementation(libs.paging.compose) 83 | implementation(libs.jbcrypt) 84 | implementation(libs.jsoup) 85 | } -------------------------------------------------------------------------------- /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. 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/com/anpe/coolbbsyou/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou 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("com.anpe.coolbbsyou", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/constant/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.constant 2 | 3 | object Constants { 4 | const val USER_AGENT ="Dalvik/2.1.0 (Linux; U; Android 7.1.2; SM-G977N Build/LMY48Z) (#Build; samsung; SM-G977N; beyond1qlteue-user 7.1.2 LMY48Z 701230529 release-keys; 7.1.2) +CoolMarket/13.3.1-2307121-universal" 5 | const val APP_LABEL = "token://com.coolapk.market/dcf01e569c1e3db93a3d0fcf191a622c" 6 | const val APP_ID = "com.coolapk.market" 7 | const val REQUEST_WIDTH = "XMLHttpRequest" 8 | const val COOKIE_PREFS = "Cookies_Prefs" 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/cookie/CookieManger.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.cookie 2 | 3 | import android.content.Context 4 | import okhttp3.Cookie 5 | import okhttp3.CookieJar 6 | import okhttp3.HttpUrl 7 | 8 | class CookieManger(context: Context): CookieJar { 9 | companion object { 10 | private val TAG = this::class.java.simpleName 11 | } 12 | 13 | private val cookieStore = PersistentCookieStore(context) 14 | 15 | override fun loadForRequest(url: HttpUrl): List { 16 | return cookieStore[url] 17 | } 18 | 19 | override fun saveFromResponse(url: HttpUrl, cookies: List) { 20 | if (cookies.isNotEmpty()) { 21 | for (item in cookies) { 22 | cookieStore.add(url, item) 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/cookie/OkHttpCookies.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.cookie 2 | 3 | import okhttp3.Cookie 4 | import java.io.IOException 5 | import java.io.ObjectInputStream 6 | import java.io.ObjectOutputStream 7 | import java.io.Serializable 8 | 9 | class OkHttpCookies(cookies: Cookie): Serializable { 10 | @Transient 11 | private val cookies: Cookie 12 | 13 | @Transient 14 | private var clientCookies: Cookie? = null 15 | 16 | init { 17 | this.cookies = cookies 18 | } 19 | 20 | fun getCookies(): Cookie? { 21 | var bestCookies: Cookie? = cookies 22 | if (clientCookies != null) { 23 | bestCookies = clientCookies 24 | } 25 | return bestCookies 26 | } 27 | 28 | @Throws(IOException::class) 29 | private fun writeObject(out: ObjectOutputStream) { 30 | out.writeObject(cookies.name) 31 | out.writeObject(cookies.value) 32 | out.writeLong(cookies.expiresAt) 33 | out.writeObject(cookies.domain) 34 | out.writeObject(cookies.path) 35 | out.writeBoolean(cookies.secure) 36 | out.writeBoolean(cookies.httpOnly) 37 | out.writeBoolean(cookies.hostOnly) 38 | out.writeBoolean(cookies.persistent) 39 | } 40 | 41 | @Throws(IOException::class, ClassNotFoundException::class) 42 | private fun readObject(`in`: ObjectInputStream) { 43 | val name = `in`.readObject() as String 44 | val value = `in`.readObject() as String 45 | val expiresAt = `in`.readLong() 46 | val domain = `in`.readObject() as String 47 | val path = `in`.readObject() as String 48 | val secure = `in`.readBoolean() 49 | val httpOnly = `in`.readBoolean() 50 | val hostOnly = `in`.readBoolean() 51 | val persistent = `in`.readBoolean() 52 | var builder = Cookie.Builder() 53 | builder = builder.name(name) 54 | builder = builder.value(value) 55 | builder = builder.expiresAt(expiresAt) 56 | builder = if (hostOnly) builder.hostOnlyDomain(domain) else builder.domain(domain) 57 | builder = builder.path(path) 58 | builder = if (secure) builder.secure() else builder 59 | builder = if (httpOnly) builder.httpOnly() else builder 60 | clientCookies = builder.build() 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/cookie/PersistentCookieStore.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.cookie 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.text.TextUtils 6 | import android.util.Log 7 | import com.anpe.coolbbsyou.constant.Constants 8 | import com.anpe.coolbbsyou.util.Utils.Companion.byteArrayToHexString 9 | import com.anpe.coolbbsyou.util.Utils.Companion.hexStringToByteArray 10 | import okhttp3.Cookie 11 | import okhttp3.HttpUrl 12 | import java.io.ByteArrayInputStream 13 | import java.io.ByteArrayOutputStream 14 | import java.io.IOException 15 | import java.io.ObjectInputStream 16 | import java.io.ObjectOutputStream 17 | import java.util.concurrent.ConcurrentHashMap 18 | 19 | open class PersistentCookieStore(context: Context) { 20 | companion object { 21 | private val TAG = this::class.java.simpleName 22 | } 23 | 24 | private val cookies: MutableMap> = hashMapOf() 25 | private val cookiePrefs: SharedPreferences 26 | 27 | init { 28 | cookiePrefs = context.getSharedPreferences(Constants.COOKIE_PREFS, 0) 29 | 30 | // 将持久化的cookies缓存到内存中 即map cookies 31 | val prefsMap = cookiePrefs.all 32 | for ((key, value) in prefsMap) { 33 | val cookieNames = TextUtils.split(value as String, ",") 34 | for (name in cookieNames) { 35 | val encodedCookie = cookiePrefs.getString(name, null) 36 | encodedCookie?.apply { 37 | val decodeCookie = this.decodeCookie() 38 | decodeCookie?.apply { 39 | if (!cookies.containsKey(key)) { 40 | cookies[key] = ConcurrentHashMap() 41 | } 42 | cookies[key]?.set(name, this) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | private fun Cookie.getCookieToken() = "${this.name}@${this.domain}" 50 | 51 | fun add(url: HttpUrl, cookie: Cookie) { 52 | val name = cookie.getCookieToken() 53 | 54 | // 将cookies缓存到内存中 如果缓存过期 就重置此cookie 55 | if (!cookie.persistent) { 56 | if (!cookies.containsKey(url.host)) { 57 | cookies[url.host] = ConcurrentHashMap() 58 | } 59 | cookies[url.host]!![name] = cookie 60 | } else { 61 | if (cookies.containsKey(url.host)) { 62 | cookies[url.host]!!.remove(name) 63 | } 64 | } 65 | 66 | // 将cookies持久化到本地 67 | val prefsWriter = cookiePrefs.edit() 68 | 69 | // 问题处 70 | cookiePrefs.getString(url.host, null).apply { 71 | if (this == null) { 72 | prefsWriter.putString(url.host, name) 73 | } else { 74 | val list = this.split(",") 75 | if (name !in list) { 76 | val content = "$this,$name" 77 | prefsWriter.putString(url.host, content) 78 | } 79 | } 80 | } 81 | // prefsWriter.putString(url.host, TextUtils.join(",", cookies[url.host]!!.keySet(cookie))) 82 | prefsWriter.putString(name, OkHttpCookies(cookie).encodeCookie()) 83 | prefsWriter.apply() 84 | } 85 | 86 | operator fun get(url: HttpUrl): List { 87 | val ret = ArrayList() 88 | val list = url.host.split(".").reversed() 89 | val host = "${list[1]}.${list[0]}" 90 | cookies.keys.forEach { 91 | if (it.indexOf(host) >= 0) { 92 | ret.addAll(cookies[it]!!.values) 93 | } 94 | } 95 | /*if (cookies.containsKey(host)) { 96 | val values = cookies[host]!!.values 97 | ret.addAll(cookies[host]!!.values) 98 | }*/ 99 | return ret 100 | } 101 | 102 | fun removeAll(): Boolean { 103 | val prefsWriter = cookiePrefs.edit() 104 | prefsWriter.clear() 105 | prefsWriter.apply() 106 | cookies.clear() 107 | return true 108 | } 109 | 110 | fun remove(url: HttpUrl, cookie: Cookie): Boolean { 111 | val name = cookie.getCookieToken() 112 | return if (cookies.containsKey(url.host) && cookies[url.host]!!.containsKey(name)) { 113 | cookies[url.host]!!.remove(name) 114 | val prefsWriter = cookiePrefs.edit() 115 | if (cookiePrefs.contains(name)) { 116 | prefsWriter.remove(name) 117 | } 118 | prefsWriter.putString(url.host, TextUtils.join(",", cookies[url.host]!!.keySet(cookie))) 119 | prefsWriter.apply() 120 | true 121 | } else { 122 | false 123 | } 124 | } 125 | 126 | fun getCookies(): List { 127 | val ret = ArrayList() 128 | for (key in cookies.keys) { 129 | ret.addAll(cookies[key]!!.values) 130 | } 131 | return ret 132 | } 133 | 134 | /** 135 | * cookies 序列化成 string 136 | * 137 | * @param cookie 要序列化的cookie 138 | * @return 序列化之后的string 139 | */ 140 | private fun OkHttpCookies.encodeCookie(): String? { 141 | val os = ByteArrayOutputStream() 142 | try { 143 | val outputStream = ObjectOutputStream(os) 144 | outputStream.writeObject(this) 145 | } catch (e: IOException) { 146 | Log.d(TAG, "IOException in encodeCookie", e) 147 | return null 148 | } 149 | return os.toByteArray().byteArrayToHexString() 150 | } 151 | 152 | /** 153 | * 将字符串反序列化成cookies 154 | * 155 | * @param cookieString cookies string 156 | * @return cookie object 157 | */ 158 | private fun String.decodeCookie(): Cookie? { 159 | val bytes = this.hexStringToByteArray() 160 | val byteArrayInputStream = ByteArrayInputStream(bytes) 161 | var cookie: Cookie? = null 162 | try { 163 | val objectInputStream = ObjectInputStream(byteArrayInputStream) 164 | cookie = (objectInputStream.readObject() as OkHttpCookies).getCookies() 165 | } catch (e: IOException) { 166 | Log.d(TAG, "IOException in decodeCookie", e) 167 | } catch (e: ClassNotFoundException) { 168 | Log.d(TAG, "ClassNotFoundException in decodeCookie", e) 169 | } 170 | return cookie 171 | } 172 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/intent/MainIntent.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.intent 2 | 3 | sealed class MainIntent { 4 | // 获取首页内容 5 | object GetIndex: MainIntent() 6 | 7 | // 获取帖子详情 8 | data class GetDetails(val id: Int): MainIntent() 9 | 10 | // 图片视图 11 | data class OpenNineGrid(val isNineGrid: Boolean): MainIntent() 12 | 13 | // 获取搜索推荐词 14 | data class GetSuggestSearch(val keyword: String): MainIntent() 15 | 16 | // 获取每日酷安 17 | data class GetTodayCool(val page: Int, val url: String): MainIntent() 18 | 19 | // 登陆 20 | data class LoginAccount(val requestHash: String, val account: String, val passwd: String, val captcha: String): MainIntent() 21 | 22 | // 登陆状态查询 23 | object LoginState: MainIntent() 24 | 25 | // 主页信息 26 | data class GetProfile(val uid: Int): MainIntent() 27 | 28 | // 获取消息 29 | object GetNotification: MainIntent() 30 | 31 | // 获取评论 32 | data class GetReply(val id: Int): MainIntent() 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/model/deviceInfo/DeviceInfo.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.model.deviceInfo 2 | 3 | data class DeviceInfo( 4 | val aid: String, 5 | val mac: String, 6 | val manuFactor: String, 7 | val brand: String, 8 | val model: String, 9 | val buildNumber: String 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/model/login/LoginEntity.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.model.login 2 | import com.google.gson.annotations.SerializedName 3 | 4 | 5 | data class LoginEntity( 6 | @SerializedName("ajaxRequest") 7 | val ajaxRequest: Boolean, 8 | @SerializedName("appName") 9 | val appName: String, 10 | @SerializedName("baseUrl") 11 | val baseUrl: String, 12 | @SerializedName("calledMethodName") 13 | val calledMethodName: String, 14 | @SerializedName("charset") 15 | val charset: String, 16 | @SerializedName("className") 17 | val className: String, 18 | @SerializedName("controllerName") 19 | val controllerName: String, 20 | @SerializedName("coolMarketClient") 21 | val coolMarketClient: Boolean, 22 | @SerializedName("displayMode") 23 | val displayMode: String, 24 | @SerializedName("error") 25 | val error: Int, 26 | @SerializedName("forward") 27 | val forward: String, 28 | @SerializedName("iosLogin") 29 | val iosLogin: Boolean, 30 | @SerializedName("isCommunity") 31 | val isCommunity: Boolean, 32 | @SerializedName("message") 33 | val message: String, 34 | @SerializedName("messageStatus") 35 | val messageStatus: Int, 36 | @SerializedName("requestAjax") 37 | val requestAjax: Boolean, 38 | @SerializedName("requestApp") 39 | val requestApp: String, 40 | @SerializedName("requestBase") 41 | val requestBase: String, 42 | @SerializedName("requestBaseUrl") 43 | val requestBaseUrl: String, 44 | @SerializedName("requestHash") 45 | val requestHash: String, 46 | @SerializedName("requestTime") 47 | val requestTime: Int, 48 | @SerializedName("SESSION") 49 | val sESSION: SESSION, 50 | @SerializedName("status") 51 | val status: Int, 52 | @SerializedName("themeName") 53 | val themeName: String 54 | ) 55 | 56 | data class SESSION( 57 | @SerializedName("isAdministrator") 58 | val isAdministrator: Boolean, 59 | @SerializedName("isLogin") 60 | val isLogin: Boolean, 61 | @SerializedName("isMember") 62 | val isMember: Boolean, 63 | @SerializedName("isSubAdmin") 64 | val isSubAdmin: Int, 65 | @SerializedName("isSuperAdministrator") 66 | val isSuperAdministrator: Boolean, 67 | @SerializedName("isTester") 68 | val isTester: Boolean, 69 | @SerializedName("status") 70 | val status: Int, 71 | @SerializedName("uid") 72 | val uid: Int, 73 | @SerializedName("username") 74 | val username: Any 75 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/model/loginState/LoginStateEntity.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.model.loginState 2 | import com.google.gson.annotations.SerializedName 3 | 4 | 5 | data class LoginStateEntity( 6 | @SerializedName("data") 7 | val `data`: Data, 8 | @SerializedName("status") 9 | val status: Int, 10 | @SerializedName("error") 11 | val error: Int, 12 | @SerializedName("message") 13 | val message: String, 14 | @SerializedName("messageStatus") 15 | val messageStatus: Int, 16 | ) 17 | 18 | data class Data( 19 | @SerializedName("adminType") 20 | val adminType: Int, 21 | @SerializedName("notifyCount") 22 | val notifyCount: NotifyCount, 23 | @SerializedName("pushId") 24 | val pushId: String, 25 | @SerializedName("subAdmin") 26 | val subAdmin: Int, 27 | @SerializedName("systemConfig") 28 | val systemConfig: SystemConfig, 29 | @SerializedName("token") 30 | val token: String, 31 | @SerializedName("uid") 32 | val uid: String, 33 | @SerializedName("userAvatar") 34 | val userAvatar: String, 35 | @SerializedName("username") 36 | val username: String 37 | ) 38 | 39 | data class NotifyCount( 40 | @SerializedName("atcommentme") 41 | val atcommentme: Int, 42 | @SerializedName("atme") 43 | val atme: Int, 44 | @SerializedName("badge") 45 | val badge: Int, 46 | @SerializedName("cloudInstall") 47 | val cloudInstall: Int, 48 | @SerializedName("commentme") 49 | val commentme: Int, 50 | @SerializedName("contacts_follow") 51 | val contactsFollow: Int, 52 | @SerializedName("dateline") 53 | val dateline: Int, 54 | @SerializedName("feedlike") 55 | val feedlike: Int, 56 | @SerializedName("message") 57 | val message: Int, 58 | @SerializedName("notification") 59 | val notification: Int 60 | ) 61 | 62 | data class SystemConfig( 63 | @SerializedName("system_config") 64 | val systemConfig: String 65 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/model/nofitication/NotificationEntity.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.model.nofitication 2 | import com.google.gson.annotations.SerializedName 3 | 4 | 5 | data class NotificationEntity( 6 | @SerializedName("data") 7 | val `data`: List 8 | ) 9 | 10 | data class Data( 11 | @SerializedName("dateline") 12 | val dateline: Int, 13 | @SerializedName("entityId") 14 | val entityId: Int, 15 | @SerializedName("entityType") 16 | val entityType: String, 17 | @SerializedName("from_type") 18 | val fromType: Int, 19 | @SerializedName("fromUserAvatar") 20 | val fromUserAvatar: String, 21 | @SerializedName("fromUserInfo") 22 | val fromUserInfo: FromUserInfo, 23 | @SerializedName("fromuid") 24 | val fromuid: Int, 25 | @SerializedName("fromusername") 26 | val fromusername: String, 27 | @SerializedName("id") 28 | val id: Int, 29 | @SerializedName("isnew") 30 | val isnew: Int, 31 | @SerializedName("list_group") 32 | val listGroup: Int, 33 | @SerializedName("note") 34 | val note: String, 35 | @SerializedName("notifyCount") 36 | val notifyCount: NotifyCount, 37 | @SerializedName("slug") 38 | val slug: String, 39 | @SerializedName("type") 40 | val type: String, 41 | @SerializedName("uid") 42 | val uid: Int, 43 | @SerializedName("url") 44 | val url: String 45 | ) 46 | 47 | data class FromUserInfo( 48 | @SerializedName("admintype") 49 | val admintype: Int, 50 | @SerializedName("avatar_cover_status") 51 | val avatarCoverStatus: Int, 52 | @SerializedName("avatar_plugin_status") 53 | val avatarPluginStatus: Int, 54 | @SerializedName("avatar_plugin_url") 55 | val avatarPluginUrl: String, 56 | @SerializedName("avatarstatus") 57 | val avatarstatus: Int, 58 | @SerializedName("block_status") 59 | val blockStatus: Int, 60 | @SerializedName("cover") 61 | val cover: String, 62 | @SerializedName("displayUsername") 63 | val displayUsername: String, 64 | @SerializedName("entityId") 65 | val entityId: Int, 66 | @SerializedName("entityType") 67 | val entityType: String, 68 | @SerializedName("experience") 69 | val experience: Int, 70 | @SerializedName("feed_plugin_open_url") 71 | val feedPluginOpenUrl: String, 72 | @SerializedName("feed_plugin_url") 73 | val feedPluginUrl: String, 74 | @SerializedName("fetchType") 75 | val fetchType: String, 76 | @SerializedName("groupid") 77 | val groupid: Int, 78 | @SerializedName("isDeveloper") 79 | val isDeveloper: Int, 80 | @SerializedName("level") 81 | val level: Int, 82 | @SerializedName("level_detail_url") 83 | val levelDetailUrl: String, 84 | @SerializedName("level_today_message") 85 | val levelTodayMessage: String, 86 | @SerializedName("logintime") 87 | val logintime: Int, 88 | @SerializedName("next_level_experience") 89 | val nextLevelExperience: Int, 90 | @SerializedName("next_level_percentage") 91 | val nextLevelPercentage: String, 92 | @SerializedName("regdate") 93 | val regdate: Int, 94 | @SerializedName("status") 95 | val status: Int, 96 | @SerializedName("uid") 97 | val uid: Int, 98 | @SerializedName("url") 99 | val url: String, 100 | @SerializedName("userAvatar") 101 | val userAvatar: String, 102 | @SerializedName("userBigAvatar") 103 | val userBigAvatar: String, 104 | @SerializedName("userSmallAvatar") 105 | val userSmallAvatar: String, 106 | @SerializedName("user_type") 107 | val userType: Int, 108 | @SerializedName("usergroupid") 109 | val usergroupid: Int, 110 | @SerializedName("username") 111 | val username: String, 112 | @SerializedName("usernamestatus") 113 | val usernamestatus: Int, 114 | @SerializedName("verify_icon") 115 | val verifyIcon: String, 116 | @SerializedName("verify_label") 117 | val verifyLabel: String, 118 | @SerializedName("verify_show_type") 119 | val verifyShowType: Int, 120 | @SerializedName("verify_status") 121 | val verifyStatus: Int, 122 | @SerializedName("verify_title") 123 | val verifyTitle: String 124 | ) 125 | 126 | data class NotifyCount( 127 | @SerializedName("atcommentme") 128 | val atcommentme: Int, 129 | @SerializedName("atme") 130 | val atme: Int, 131 | @SerializedName("badge") 132 | val badge: Int, 133 | @SerializedName("cloudInstall") 134 | val cloudInstall: Int, 135 | @SerializedName("commentme") 136 | val commentme: Int, 137 | @SerializedName("contacts_follow") 138 | val contactsFollow: Int, 139 | @SerializedName("dateline") 140 | val dateline: Int, 141 | @SerializedName("feedlike") 142 | val feedlike: Int, 143 | @SerializedName("message") 144 | val message: Int, 145 | @SerializedName("notification") 146 | val notification: Int 147 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/model/profile/ProfileEntity.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.model.profile 2 | import com.google.gson.annotations.SerializedName 3 | 4 | 5 | data class ProfileEntity( 6 | @SerializedName("data") 7 | val `data`: Data 8 | ) 9 | 10 | data class Data( 11 | @SerializedName("admintype") 12 | val admintype: Int, 13 | @SerializedName("albumFavNum") 14 | val albumFavNum: Int, 15 | @SerializedName("albumNum") 16 | val albumNum: Int, 17 | @SerializedName("apkCommentNum") 18 | val apkCommentNum: Int, 19 | @SerializedName("apkDevNum") 20 | val apkDevNum: Int, 21 | @SerializedName("apkFollowNum") 22 | val apkFollowNum: Int, 23 | @SerializedName("apkRatingNum") 24 | val apkRatingNum: Int, 25 | @SerializedName("astro") 26 | val astro: String, 27 | @SerializedName("avatar_cover_status") 28 | val avatarCoverStatus: Int, 29 | @SerializedName("avatar_plugin_status") 30 | val avatarPluginStatus: Int, 31 | @SerializedName("avatar_plugin_url") 32 | val avatarPluginUrl: String, 33 | @SerializedName("avatarstatus") 34 | val avatarstatus: Int, 35 | @SerializedName("be_like_num") 36 | val beLikeNum: Int, 37 | @SerializedName("bio") 38 | val bio: String, 39 | @SerializedName("birthday") 40 | val birthday: Int, 41 | @SerializedName("birthmonth") 42 | val birthmonth: Int, 43 | @SerializedName("birthyear") 44 | val birthyear: Int, 45 | @SerializedName("blog") 46 | val blog: String, 47 | @SerializedName("city") 48 | val city: String, 49 | @SerializedName("cover") 50 | val cover: String, 51 | @SerializedName("discoveryNum") 52 | val discoveryNum: Int, 53 | @SerializedName("displayUsername") 54 | val displayUsername: String, 55 | @SerializedName("email") 56 | val email: String, 57 | @SerializedName("emailstatus") 58 | val emailstatus: Int, 59 | @SerializedName("entityId") 60 | val entityId: Int, 61 | @SerializedName("entityType") 62 | val entityType: String, 63 | @SerializedName("experience") 64 | val experience: Int, 65 | @SerializedName("fans") 66 | val fans: Int, 67 | @SerializedName("feed") 68 | val feed: Int, 69 | @SerializedName("feed_plugin_open_url") 70 | val feedPluginOpenUrl: String, 71 | @SerializedName("feed_plugin_url") 72 | val feedPluginUrl: String, 73 | @SerializedName("fetchType") 74 | val fetchType: String, 75 | @SerializedName("follow") 76 | val follow: Int, 77 | @SerializedName("gender") 78 | val gender: Int, 79 | @SerializedName("goodsNum") 80 | val goodsNum: Int, 81 | @SerializedName("groupid") 82 | val groupid: Int, 83 | @SerializedName("isBeBlackList") 84 | val isBeBlackList: Int, 85 | @SerializedName("isBlackList") 86 | val isBlackList: Int, 87 | @SerializedName("isDeveloper") 88 | val isDeveloper: Int, 89 | @SerializedName("isFans") 90 | val isFans: Int, 91 | @SerializedName("isFollow") 92 | val isFollow: Int, 93 | @SerializedName("isIgnoreList") 94 | val isIgnoreList: Int, 95 | @SerializedName("isLimitList") 96 | val isLimitList: Int, 97 | @SerializedName("level") 98 | val level: Int, 99 | @SerializedName("level_detail_url") 100 | val levelDetailUrl: String, 101 | @SerializedName("level_today_message") 102 | val levelTodayMessage: String, 103 | @SerializedName("logintime") 104 | val logintime: Int, 105 | @SerializedName("mobile") 106 | val mobile: String, 107 | @SerializedName("mobilestatus") 108 | val mobilestatus: Int, 109 | @SerializedName("next_level_experience") 110 | val nextLevelExperience: Int, 111 | @SerializedName("next_level_percentage") 112 | val nextLevelPercentage: String, 113 | @SerializedName("province") 114 | val province: String, 115 | @SerializedName("regdate") 116 | val regdate: Int, 117 | @SerializedName("replyNum") 118 | val replyNum: Int, 119 | @SerializedName("selectedTab") 120 | val selectedTab: String, 121 | @SerializedName("status") 122 | val status: Int, 123 | @SerializedName("uid") 124 | val uid: Int, 125 | @SerializedName("url") 126 | val url: String, 127 | @SerializedName("userAvatar") 128 | val userAvatar: String, 129 | @SerializedName("userBigAvatar") 130 | val userBigAvatar: String, 131 | @SerializedName("userSmallAvatar") 132 | val userSmallAvatar: String, 133 | @SerializedName("user_type") 134 | val userType: Int, 135 | @SerializedName("usergroupid") 136 | val usergroupid: Int, 137 | @SerializedName("username") 138 | val username: String, 139 | @SerializedName("usernamestatus") 140 | val usernamestatus: Int, 141 | @SerializedName("verify_icon") 142 | val verifyIcon: String, 143 | @SerializedName("verify_label") 144 | val verifyLabel: String, 145 | @SerializedName("verify_show_type") 146 | val verifyShowType: Int, 147 | @SerializedName("verify_status") 148 | val verifyStatus: Int, 149 | @SerializedName("verify_title") 150 | val verifyTitle: String, 151 | @SerializedName("weibo") 152 | val weibo: String 153 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/model/reply/ReplyEntity.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.model.reply 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ReplyEntity( 6 | @SerializedName("data") 7 | val `data`: List 8 | ) 9 | 10 | data class Data( 11 | @SerializedName("avatarFetchType") 12 | val avatarFetchType: String, 13 | @SerializedName("block_status") 14 | val blockStatus: Int, 15 | @SerializedName("burynum") 16 | val burynum: Int, 17 | @SerializedName("dateline") 18 | val dateline: Int, 19 | @SerializedName("entityId") 20 | val entityId: Int, 21 | @SerializedName("entityTemplate") 22 | val entityTemplate: String, 23 | @SerializedName("entityType") 24 | val entityType: String, 25 | @SerializedName("extra_fromApi") 26 | val extraFromApi: String, 27 | @SerializedName("feedUid") 28 | val feedUid: Int, 29 | @SerializedName("fetchType") 30 | val fetchType: String, 31 | @SerializedName("fid") 32 | val fid: Int, 33 | @SerializedName("ftype") 34 | val ftype: Int, 35 | @SerializedName("id") 36 | val id: Int, 37 | @SerializedName("infoHtml") 38 | val infoHtml: String, 39 | @SerializedName("isFeedAuthor") 40 | val isFeedAuthor: Int, 41 | @SerializedName("is_folded") 42 | val isFolded: Int, 43 | @SerializedName("lastupdate") 44 | val lastupdate: Int, 45 | @SerializedName("likenum") 46 | val likenum: Int, 47 | @SerializedName("message") 48 | val message: String, 49 | @SerializedName("message_sp") 50 | val messageSp: String, 51 | @SerializedName("message_status") 52 | val messageStatus: Int, 53 | @SerializedName("pic") 54 | val pic: String, 55 | @SerializedName("picArr") 56 | val picArr: List, 57 | @SerializedName("rank_score") 58 | val rankScore: Int, 59 | @SerializedName("recent_reply_ids") 60 | val recentReplyIds: String, 61 | @SerializedName("replynum") 62 | val replynum: Int, 63 | @SerializedName("reportnum") 64 | val reportnum: Int, 65 | @SerializedName("rid") 66 | val rid: Int, 67 | @SerializedName("rrid") 68 | val rrid: Int, 69 | @SerializedName("ruid") 70 | val ruid: Int, 71 | @SerializedName("rusername") 72 | val rusername: String, 73 | @SerializedName("status") 74 | val status: Int, 75 | @SerializedName("uid") 76 | val uid: Int, 77 | @SerializedName("userAction") 78 | val userAction: UserAction, 79 | @SerializedName("userAvatar") 80 | val userAvatar: String, 81 | @SerializedName("userInfo") 82 | val userInfo: UserInfo, 83 | @SerializedName("username") 84 | val username: String 85 | ) 86 | 87 | data class UserAction( 88 | @SerializedName("like") 89 | val like: Int 90 | ) 91 | 92 | data class UserInfo( 93 | @SerializedName("admintype") 94 | val admintype: Int, 95 | @SerializedName("avatar_cover_status") 96 | val avatarCoverStatus: Int, 97 | @SerializedName("avatar_plugin_status") 98 | val avatarPluginStatus: Int, 99 | @SerializedName("avatar_plugin_url") 100 | val avatarPluginUrl: String, 101 | @SerializedName("avatarstatus") 102 | val avatarstatus: Int, 103 | @SerializedName("block_status") 104 | val blockStatus: Int, 105 | @SerializedName("cover") 106 | val cover: String, 107 | @SerializedName("displayUsername") 108 | val displayUsername: String, 109 | @SerializedName("entityId") 110 | val entityId: Int, 111 | @SerializedName("entityType") 112 | val entityType: String, 113 | @SerializedName("experience") 114 | val experience: Int, 115 | @SerializedName("feed_plugin_open_url") 116 | val feedPluginOpenUrl: String, 117 | @SerializedName("feed_plugin_url") 118 | val feedPluginUrl: String, 119 | @SerializedName("fetchType") 120 | val fetchType: String, 121 | @SerializedName("groupid") 122 | val groupid: Int, 123 | @SerializedName("isDeveloper") 124 | val isDeveloper: Int, 125 | @SerializedName("level") 126 | val level: Int, 127 | @SerializedName("level_detail_url") 128 | val levelDetailUrl: String, 129 | @SerializedName("level_today_message") 130 | val levelTodayMessage: String, 131 | @SerializedName("logintime") 132 | val logintime: Int, 133 | @SerializedName("next_level_experience") 134 | val nextLevelExperience: Int, 135 | @SerializedName("next_level_percentage") 136 | val nextLevelPercentage: String, 137 | @SerializedName("regdate") 138 | val regdate: Int, 139 | @SerializedName("status") 140 | val status: Int, 141 | @SerializedName("uid") 142 | val uid: Int, 143 | @SerializedName("url") 144 | val url: String, 145 | @SerializedName("userAvatar") 146 | val userAvatar: String, 147 | @SerializedName("userBigAvatar") 148 | val userBigAvatar: String, 149 | @SerializedName("userSmallAvatar") 150 | val userSmallAvatar: String, 151 | @SerializedName("user_type") 152 | val userType: Int, 153 | @SerializedName("usergroupid") 154 | val usergroupid: Int, 155 | @SerializedName("username") 156 | val username: String, 157 | @SerializedName("usernamestatus") 158 | val usernamestatus: Int, 159 | @SerializedName("verify_icon") 160 | val verifyIcon: String, 161 | @SerializedName("verify_label") 162 | val verifyLabel: String, 163 | @SerializedName("verify_show_type") 164 | val verifyShowType: Int, 165 | @SerializedName("verify_status") 166 | val verifyStatus: Int, 167 | @SerializedName("verify_title") 168 | val verifyTitle: String 169 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/model/suggest/SuggestSearchEntity.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.model.suggest 2 | import com.google.gson.annotations.SerializedName 3 | 4 | 5 | data class SuggestSearchEntity( 6 | @SerializedName("data") 7 | val `data`: List 8 | ) 9 | 10 | data class Data( 11 | @SerializedName("adminscore") 12 | val adminscore: String, 13 | @SerializedName("allow_rating") 14 | val allowRating: Int, 15 | @SerializedName("apkRomVersion") 16 | val apkRomVersion: String, 17 | @SerializedName("apkTypeName") 18 | val apkTypeName: String, 19 | @SerializedName("apkTypeUrl") 20 | val apkTypeUrl: String, 21 | @SerializedName("apkUrl") 22 | val apkUrl: String, 23 | @SerializedName("apklength") 24 | val apklength: Int, 25 | @SerializedName("apkmd5") 26 | val apkmd5: String, 27 | @SerializedName("apkname") 28 | val apkname: String, 29 | @SerializedName("apkname2") 30 | val apkname2: String, 31 | @SerializedName("apksize") 32 | val apksize: String, 33 | @SerializedName("apktype") 34 | val apktype: Int, 35 | @SerializedName("apkversioncode") 36 | val apkversioncode: Int, 37 | @SerializedName("apkversionname") 38 | val apkversionname: String, 39 | @SerializedName("catDir") 40 | val catDir: String, 41 | @SerializedName("catName") 42 | val catName: String, 43 | @SerializedName("catid") 44 | val catid: Int, 45 | @SerializedName("changelog") 46 | val changelog: String, 47 | @SerializedName("comment_block_num") 48 | val commentBlockNum: Int, 49 | @SerializedName("commentCount") 50 | val commentCount: Int, 51 | @SerializedName("comment_status") 52 | val commentStatus: Int, 53 | @SerializedName("commentStatusText") 54 | val commentStatusText: String, 55 | @SerializedName("commentnum") 56 | val commentnum: Int, 57 | @SerializedName("cover") 58 | val cover: String, 59 | @SerializedName("description") 60 | val description: String, 61 | @SerializedName("developername") 62 | val developername: String, 63 | @SerializedName("developeruid") 64 | val developeruid: Int, 65 | @SerializedName("digest") 66 | val digest: Int, 67 | @SerializedName("downCount") 68 | val downCount: String, 69 | @SerializedName("downnum") 70 | val downnum: Int, 71 | @SerializedName("entityId") 72 | val entityId: Int, 73 | @SerializedName("entityType") 74 | val entityType: String, 75 | @SerializedName("extraFlag") 76 | val extraFlag: String, 77 | @SerializedName("favnum") 78 | val favnum: Int, 79 | @SerializedName("followCount") 80 | val followCount: Int, 81 | @SerializedName("follownum") 82 | val follownum: Int, 83 | @SerializedName("get_timewit_cpc") 84 | val getTimewitCpc: Int, 85 | @SerializedName("hot_num") 86 | val hotNum: Int, 87 | @SerializedName("hot_num_txt") 88 | val hotNumTxt: String, 89 | @SerializedName("hotlabel") 90 | val hotlabel: String, 91 | @SerializedName("id") 92 | val id: Int, 93 | @SerializedName("is_coolapk_cpa") 94 | val isCoolapkCpa: Int, 95 | @SerializedName("is_forum_app") 96 | val isForumApp: Int, 97 | @SerializedName("isbiz") 98 | val isbiz: Int, 99 | @SerializedName("iscps") 100 | val iscps: Int, 101 | @SerializedName("ishot") 102 | val ishot: Int, 103 | @SerializedName("ishp") 104 | val ishp: Int, 105 | @SerializedName("keywords") 106 | val keywords: String, 107 | @SerializedName("last_comment_update") 108 | val lastCommentUpdate: Int, 109 | @SerializedName("lastupdate") 110 | val lastupdate: Int, 111 | @SerializedName("logo") 112 | val logo: String, 113 | @SerializedName("open_link") 114 | val openLink: String, 115 | @SerializedName("packageName") 116 | val packageName: String, 117 | @SerializedName("pubStatusText") 118 | val pubStatusText: String, 119 | @SerializedName("pubdate") 120 | val pubdate: Int, 121 | @SerializedName("recommend") 122 | val recommend: Int, 123 | @SerializedName("replyCount") 124 | val replyCount: Int, 125 | @SerializedName("replynum") 126 | val replynum: Int, 127 | @SerializedName("romversion") 128 | val romversion: String, 129 | @SerializedName("score") 130 | val score: String, 131 | @SerializedName("score_v10") 132 | val scoreV10: Double, 133 | @SerializedName("sdkversion") 134 | val sdkversion: Int, 135 | @SerializedName("shortTags") 136 | val shortTags: String, 137 | @SerializedName("shortlabel") 138 | val shortlabel: String, 139 | @SerializedName("shorttitle") 140 | val shorttitle: String, 141 | @SerializedName("star") 142 | val star: Int, 143 | @SerializedName("status") 144 | val status: Int, 145 | @SerializedName("statusText") 146 | val statusText: String, 147 | @SerializedName("target_id") 148 | val targetId: String, 149 | @SerializedName("title") 150 | val title: String, 151 | @SerializedName("updateFlag") 152 | val updateFlag: String, 153 | @SerializedName("url") 154 | val url: String, 155 | @SerializedName("version") 156 | val version: String, 157 | @SerializedName("voteCount") 158 | val voteCount: Int, 159 | @SerializedName("votenum") 160 | val votenum: Int 161 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/repository/ApiRepository.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.repository 2 | 3 | import android.content.Context 4 | import com.anpe.coolbbsyou.network.data.model.index.IndexEntity 5 | import com.anpe.coolbbsyou.network.service.ApiService 6 | import com.anpe.coolbbsyou.network.service.ApiServiceTwo 7 | import com.anpe.coolbbsyou.util.MyApplication 8 | import com.anpe.coolbbsyou.util.TokenDeviceUtils.Companion.getLastingInstallTime 9 | 10 | class ApiRepository(context: Context = MyApplication.context) { 11 | private val api = ApiService.getSerVice(context) 12 | private val apiCall = ApiServiceTwo.getSerVice(context) 13 | 14 | private val installTime = getLastingInstallTime(context) 15 | 16 | private var firstLauncher = 1 17 | 18 | suspend fun getIndex(page: Int, lastItem: Int? = null): IndexEntity { 19 | val indexEntity = api.getIndex( 20 | page = page, 21 | lastItem = lastItem, 22 | firstLaunch = firstLauncher, 23 | installTime = installTime 24 | ) 25 | 26 | if (firstLauncher == 1) { 27 | firstLauncher = 0 28 | } 29 | 30 | return indexEntity 31 | } 32 | 33 | fun getRequestHash() = apiCall.getRequestHash() 34 | 35 | suspend fun getDetails(id: Int) = api.getDetails(id = id) 36 | 37 | suspend fun getTodayCool(page: Int, url: String) = api.getTodayCool(page = page, url = url) 38 | 39 | suspend fun getSuggestSearch(keyword: String) = api.getSuggestSearch(searchValue = keyword) 40 | 41 | suspend fun postAccount(requestHash: String, login: String, password: String) = api.postAccount( 42 | requestHash = requestHash, 43 | login = login, 44 | password = password, 45 | ) 46 | 47 | suspend fun getNotification(page: Int) = api.getNotification(page = page) 48 | 49 | suspend fun getLoginState() = api.getLoginState() 50 | 51 | suspend fun getProfile(uid: Int) = api.getProfile(uid = uid) 52 | 53 | suspend fun getReply(id: Int, page: Int, listType: String = "lastupdate_desc") = 54 | api.getReply(id = id, listType = listType, page = page) 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/source/IndexSource.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.source 2 | 3 | import android.util.Log 4 | import androidx.paging.PagingSource 5 | import androidx.paging.PagingState 6 | import com.anpe.coolbbsyou.network.data.model.index.Data 7 | import com.anpe.coolbbsyou.network.data.repository.ApiRepository 8 | 9 | class IndexSource(private val repository: ApiRepository) : PagingSource() { 10 | companion object { 11 | private val TAG = IndexSource::class.java.simpleName 12 | } 13 | 14 | private var lastItem = -1 15 | 16 | override fun getRefreshKey(state: PagingState): Int? { 17 | Log.d(TAG, "getRefreshKey: $state") 18 | return null 19 | } 20 | 21 | override suspend fun load(params: LoadParams): LoadResult { 22 | return try { 23 | val currentPage = params.key ?: 1 24 | 25 | Log.d(TAG, "load0: $lastItem") 26 | val indexEntity = if (lastItem == -1) { 27 | val indexEntity = repository.getIndex(currentPage) 28 | lastItem = indexEntity.data[indexEntity.data.lastIndex - 1].id 29 | Log.d(TAG, "load0-0: $lastItem") 30 | indexEntity 31 | } else { 32 | val indexEntity = ApiRepository().getIndex(currentPage, lastItem) 33 | lastItem = indexEntity.data.last().id 34 | Log.d(TAG, "load0-1: $lastItem") 35 | indexEntity 36 | } 37 | 38 | val nextPage = if (currentPage == 10) { 39 | null 40 | } else { 41 | currentPage + 1 42 | } 43 | 44 | LoadResult.Page(indexEntity.data, null, nextPage) 45 | } catch (e: Exception) { 46 | LoadResult.Error(throwable = e) 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/source/NotificationSource.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.source 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.anpe.coolbbsyou.network.data.model.nofitication.Data 6 | import com.anpe.coolbbsyou.network.data.repository.ApiRepository 7 | 8 | class NotificationSource(private val repository: ApiRepository): PagingSource() { 9 | override fun getRefreshKey(state: PagingState): Int? { 10 | return null 11 | } 12 | 13 | override suspend fun load(params: LoadParams): LoadResult { 14 | return try { 15 | val currentPage = params.key ?: 1 16 | 17 | val notificationEntity = repository.getNotification(currentPage) 18 | 19 | val nextPage = if (notificationEntity.data.isEmpty()) { 20 | null 21 | } else { 22 | currentPage + 1 23 | } 24 | 25 | LoadResult.Page(notificationEntity.data, null, nextPage) 26 | } catch (e: Exception) { 27 | LoadResult.Error(throwable = e) 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/source/ReplySource.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.source 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.anpe.coolbbsyou.network.data.model.reply.Data 6 | import com.anpe.coolbbsyou.network.data.repository.ApiRepository 7 | 8 | class ReplySource(private val repository: ApiRepository, val id: Int): PagingSource() { 9 | override fun getRefreshKey(state: PagingState): Int? { 10 | return null 11 | } 12 | 13 | override suspend fun load(params: LoadParams): LoadResult { 14 | return try{ 15 | val currentPage = params.key ?: 1 16 | 17 | val replyEntity = repository.getReply(id = id, page = currentPage) 18 | 19 | val nextPage = currentPage + 1 20 | 21 | LoadResult.Page(replyEntity.data, null, nextPage) 22 | } catch (e: Exception) { 23 | LoadResult.Error(throwable = e) 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/state/DetailsState.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.state 2 | 3 | import com.anpe.coolbbsyou.network.data.model.details.DetailsEntity 4 | 5 | sealed class DetailsState { 6 | object Idle: DetailsState() 7 | 8 | object Loading: DetailsState() 9 | 10 | data class Success(val detailsEntity: DetailsEntity): DetailsState() 11 | 12 | data class Error(val e: String): DetailsState() 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/state/IndexImageState.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.state 2 | 3 | sealed class IndexImageState { 4 | // 九宫格图片 5 | object NineGrid: IndexImageState() 6 | 7 | // 横滑图片排列 8 | object ImageRow: IndexImageState() 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/state/IndexState.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.state 2 | 3 | import androidx.paging.PagingData 4 | import com.anpe.coolbbsyou.network.data.model.index.Data 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | sealed class IndexState { 8 | object Idle: IndexState() 9 | 10 | object Loading: IndexState() 11 | 12 | data class Success(val pager: Flow>): IndexState() 13 | 14 | data class Error(val error: String): IndexState() 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/state/LoginInfoState.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.state 2 | 3 | import com.anpe.coolbbsyou.network.data.model.loginState.LoginStateEntity 4 | 5 | sealed class LoginInfoState { 6 | object Idle: LoginInfoState() 7 | 8 | object Loading: LoginInfoState() 9 | 10 | data class Success(val loginStateEntity: LoginStateEntity): LoginInfoState() 11 | 12 | data class Error(val e: String): LoginInfoState() 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/state/LoginState.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.state 2 | 3 | import com.anpe.coolbbsyou.network.data.model.login.LoginEntity 4 | 5 | sealed class LoginState { 6 | object Idle: LoginState() 7 | object LoggingIn: LoginState() 8 | data class Success(val loginEntity: LoginEntity): LoginState() 9 | data class Error(val error: String): LoginState() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/state/NotificationState.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.state 2 | 3 | import androidx.paging.PagingData 4 | import com.anpe.coolbbsyou.network.data.model.nofitication.Data 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | sealed class NotificationState { 8 | object Idle: NotificationState() 9 | 10 | object Loading: NotificationState() 11 | 12 | data class Success(val pager: Flow>): NotificationState() 13 | 14 | data class Error(val e: String): NotificationState() 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/state/ProfileState.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.state 2 | 3 | import com.anpe.coolbbsyou.network.data.model.profile.ProfileEntity 4 | 5 | sealed class ProfileState { 6 | object Idle: ProfileState() 7 | 8 | object Loading: ProfileState() 9 | 10 | data class Success(val profileEntity: ProfileEntity): ProfileState() 11 | 12 | data class Error(val e: String): ProfileState() 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/state/ReplyState.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.state 2 | 3 | import androidx.paging.PagingData 4 | import com.anpe.coolbbsyou.network.data.model.reply.Data 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | sealed class ReplyState { 8 | object Idle: ReplyState() 9 | 10 | object Loading: ReplyState() 11 | 12 | data class Success(val pager: Flow>): ReplyState() 13 | 14 | data class Error(val e: String): ReplyState() 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/state/SuggestState.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.state 2 | 3 | import com.anpe.coolbbsyou.network.data.model.suggest.SuggestSearchEntity 4 | 5 | sealed class SuggestState { 6 | object Idle: SuggestState() 7 | 8 | object Loading: SuggestState() 9 | 10 | data class Success(val suggestSearchEntity: SuggestSearchEntity): SuggestState() 11 | 12 | data class Error(val e: String): SuggestState() 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/data/state/TodayState.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.data.state 2 | 3 | import com.anpe.coolbbsyou.network.data.model.today.TodayCoolEntity 4 | 5 | sealed class TodayState { 6 | object Idle: TodayState() 7 | 8 | object Loading: TodayState() 9 | 10 | data class Success(val todayCoolEntity: TodayCoolEntity): TodayState() 11 | 12 | data class Error(val e: String): TodayState() 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/service/ApiService.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.service 2 | 3 | import android.content.Context 4 | import com.anpe.coolbbsyou.constant.Constants 5 | import com.anpe.coolbbsyou.network.data.cookie.CookieManger 6 | import com.anpe.coolbbsyou.network.data.model.details.DetailsEntity 7 | import com.anpe.coolbbsyou.network.data.model.index.IndexEntity 8 | import com.anpe.coolbbsyou.network.data.model.login.LoginEntity 9 | import com.anpe.coolbbsyou.network.data.model.loginState.LoginStateEntity 10 | import com.anpe.coolbbsyou.network.data.model.nofitication.NotificationEntity 11 | import com.anpe.coolbbsyou.network.data.model.profile.ProfileEntity 12 | import com.anpe.coolbbsyou.network.data.model.reply.ReplyEntity 13 | import com.anpe.coolbbsyou.network.data.model.suggest.SuggestSearchEntity 14 | import com.anpe.coolbbsyou.network.data.model.today.TodayCoolEntity 15 | import com.anpe.coolbbsyou.util.LoginUtils 16 | import com.anpe.coolbbsyou.util.MyApplication 17 | import com.anpe.coolbbsyou.util.TokenDeviceUtils 18 | import com.anpe.coolbbsyou.util.TokenDeviceUtils.Companion.getTokenV2 19 | import okhttp3.OkHttpClient 20 | import retrofit2.Retrofit 21 | import retrofit2.converter.gson.GsonConverterFactory 22 | import retrofit2.http.Field 23 | import retrofit2.http.FormUrlEncoded 24 | import retrofit2.http.GET 25 | import retrofit2.http.Header 26 | import retrofit2.http.POST 27 | import retrofit2.http.Query 28 | import java.util.concurrent.TimeUnit 29 | 30 | interface ApiService { 31 | companion object { 32 | private const val API_URL = "https://api.coolapk.com" 33 | private const val API2_URL = "https://api2.coolapk.com" 34 | private const val ACCOUNT_URL = "https://account.coolapk.com" 35 | 36 | private val deviceCode = TokenDeviceUtils.getLastingDeviceCode(MyApplication.context) 37 | private val deviceToken = deviceCode.getTokenV2() 38 | 39 | private var service: ApiService? = null 40 | 41 | fun getSerVice(context: Context): ApiService { 42 | if (service == null) { 43 | val client = OkHttpClient.Builder() 44 | .cookieJar(CookieManger(context)) 45 | .callTimeout(5, TimeUnit.SECONDS) 46 | .build() 47 | 48 | val retrofit = Retrofit.Builder() 49 | .baseUrl(API2_URL) 50 | .client(client) 51 | .addConverterFactory(GsonConverterFactory.create()) 52 | .build() 53 | 54 | return retrofit.create(ApiService::class.java) 55 | } 56 | 57 | return service as ApiService 58 | } 59 | } 60 | 61 | @GET("/v6/main/indexV8") 62 | suspend fun getIndex( 63 | @Header("X-Requested-With") requestedWith: String = Constants.REQUEST_WIDTH, 64 | @Header("X-App-Id") appId: String = Constants.APP_ID, 65 | @Header("X-App-Device") device: String = deviceCode, 66 | @Header("X-App-Token") token: String = deviceToken, 67 | @Query("ids") ids: String = "", 68 | @Query("installTime") installTime: String, 69 | @Query("lastItem") lastItem: Int?, 70 | @Query("page") page: Int, 71 | @Query("firstLaunch") firstLaunch: Int 72 | 73 | ): IndexEntity 74 | 75 | @GET("/v6/feed/detail") 76 | suspend fun getDetails( 77 | @Header("X-Requested-With") requestedWith: String = Constants.REQUEST_WIDTH, 78 | @Header("X-App-Id") appId: String = Constants.APP_ID, 79 | @Header("X-App-Device") device: String = deviceCode, 80 | @Header("X-App-Token") token: String = deviceToken, 81 | @Query("id") id: Int 82 | ): DetailsEntity 83 | 84 | @GET("$API_URL/v6/page/dataList") 85 | suspend fun getTodayCool( 86 | @Header("X-Requested-With") requestedWith: String = Constants.REQUEST_WIDTH, 87 | @Header("X-App-Id") appId: String = Constants.APP_ID, 88 | @Header("X-App-Device") device: String = deviceCode, 89 | @Header("X-App-Token") token: String = deviceToken, 90 | @Query("page") page: Int = 1, 91 | @Query("url") url: String 92 | ): TodayCoolEntity 93 | 94 | @GET("$API_URL/v6/search/suggestSearchWordsNew") 95 | suspend fun getSuggestSearch( 96 | @Header("X-Requested-With") requestedWith: String = Constants.REQUEST_WIDTH, 97 | @Header("X-App-Id") appId: String = Constants.APP_ID, 98 | @Header("X-App-Device") device: String = deviceCode, 99 | @Header("X-App-Token") token: String = deviceToken, 100 | @Query("type") type: String = "app", 101 | @Query("searchValue") searchValue: String 102 | ): SuggestSearchEntity 103 | 104 | @FormUrlEncoded 105 | @POST("$ACCOUNT_URL/auth/loginByCoolApk") 106 | suspend fun postAccount( 107 | @Header("X-Requested-With") requestedWith: String = "XMLHttpRequest", 108 | @Field("submit") submit: Int = 1, 109 | @Field("randomNumber") randomNumber: String = LoginUtils.createRandomNumber(), 110 | @Field("requestHash") requestHash: String, 111 | @Field("login") login: String, 112 | @Field("password") password: String, 113 | @Field("captcha") captcha: String = "", 114 | @Field("code") code: String = "", 115 | ): LoginEntity 116 | 117 | @GET("$API_URL/v6/notification/list") 118 | suspend fun getNotification( 119 | @Header("X-Requested-With") requestedWith: String = "XMLHttpRequest", 120 | @Header("X-App-Id") appId: String = "com.coolapk.market", 121 | @Header("X-App-Device") device: String = deviceCode, 122 | @Header("X-App-Token") token: String = deviceToken, 123 | @Query("page") page: Int 124 | ): NotificationEntity 125 | 126 | @GET("$API_URL/v6/account/checkLoginInfo") 127 | suspend fun getLoginState( 128 | @Header("X-Requested-With") requestedWith: String = "XMLHttpRequest", 129 | @Header("X-App-Id") appId: String = "com.coolapk.market", 130 | @Header("X-App-Device") device: String = deviceCode, 131 | @Header("X-App-Token") token: String = deviceToken, 132 | ): LoginStateEntity 133 | 134 | @GET("$API_URL/v6/user/profile") 135 | suspend fun getProfile( 136 | @Header("X-Requested-With") requestedWith: String = "XMLHttpRequest", 137 | @Header("X-App-Id") appId: String = "com.coolapk.market", 138 | @Header("X-App-Device") device: String = deviceCode, 139 | @Header("X-App-Token") token: String = deviceToken, 140 | @Query("uid") uid: Int 141 | ): ProfileEntity 142 | 143 | @GET("/v6/feed/replyList") 144 | suspend fun getReply( 145 | @Header("X-Requested-With") requestedWith: String = "XMLHttpRequest", 146 | @Header("X-App-Id") appId: String = "com.coolapk.market", 147 | @Header("X-App-Device") device: String = deviceCode, 148 | @Header("X-App-Token") token: String = deviceToken, 149 | @Query("id") id: Int, 150 | @Query("listType") listType: String, 151 | @Query("page") page: Int 152 | ): ReplyEntity 153 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/network/service/ApiServiceTwo.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.network.service 2 | 3 | import android.content.Context 4 | import com.anpe.coolbbsyou.network.data.cookie.CookieManger 5 | import okhttp3.OkHttpClient 6 | import okhttp3.ResponseBody 7 | import retrofit2.Call 8 | import retrofit2.Retrofit 9 | import retrofit2.http.GET 10 | import retrofit2.http.Header 11 | import java.util.concurrent.TimeUnit 12 | 13 | interface ApiServiceTwo { 14 | companion object { 15 | private const val API_URL = "https://api.coolapk.com" 16 | private const val API2_URL = "https://api2.coolapk.com" 17 | private const val ACCOUNT_URL = "https://account.coolapk.com" 18 | 19 | private var service: ApiServiceTwo? = null 20 | 21 | fun getSerVice(context: Context): ApiServiceTwo { 22 | if (service == null) { 23 | val client = OkHttpClient.Builder() 24 | .cookieJar(CookieManger(context)) 25 | .callTimeout(5, TimeUnit.SECONDS) 26 | .build() 27 | 28 | val retrofit = Retrofit.Builder() 29 | .baseUrl(ACCOUNT_URL) 30 | .client(client) 31 | .build() 32 | 33 | return retrofit.create(ApiServiceTwo::class.java) 34 | } 35 | 36 | return service as ApiServiceTwo 37 | } 38 | } 39 | 40 | @GET("/auth/loginByCoolApk") 41 | fun getRequestHash( 42 | @Header("X-Requested-With") requestedWith: String = "com.coolapk.market" 43 | ): Call 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/innerScreen/manager/InnerScreenManager.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.innerScreen.manager 2 | 3 | import androidx.annotation.StringRes 4 | import com.anpe.coolbbsyou.R 5 | 6 | sealed class InnerScreenManager(val route: String, @StringRes val resourceId: Int) { 7 | object HomeInnerScreen: InnerScreenManager("HomeInnerScreen", R.string.home_inner_screen) 8 | 9 | object TodaySelectionInnerScreen: InnerScreenManager("TodaySelectionScreen", R.string.today_selection_inner_screen) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.main 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.isSystemInDarkTheme 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Surface 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.core.view.WindowCompat 13 | import androidx.lifecycle.viewmodel.compose.viewModel 14 | import androidx.navigation.compose.NavHost 15 | import androidx.navigation.compose.composable 16 | import androidx.navigation.compose.rememberNavController 17 | import com.anpe.coolbbsyou.ui.screen.DetailsScreen 18 | import com.anpe.coolbbsyou.ui.screen.LoginScreen 19 | import com.anpe.coolbbsyou.ui.screen.MainScreen 20 | import com.anpe.coolbbsyou.ui.screen.SplashScreen 21 | import com.anpe.coolbbsyou.ui.screen.manager.ScreenManager 22 | import com.anpe.coolbbsyou.ui.theme.CoolbbsYouTheme 23 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 24 | 25 | class MainActivity : ComponentActivity() { 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | 29 | WindowCompat.setDecorFitsSystemWindows(window, false) 30 | 31 | setContent { 32 | CoolbbsYouTheme { 33 | rememberSystemUiController().run { 34 | setStatusBarColor( 35 | color = Color.Transparent, 36 | darkIcons = !isSystemInDarkTheme() 37 | ) 38 | setNavigationBarColor( 39 | color = Color.Transparent, 40 | darkIcons = !isSystemInDarkTheme(), 41 | navigationBarContrastEnforced = false 42 | ) 43 | } 44 | 45 | Surface( 46 | modifier = Modifier.fillMaxSize(), 47 | color = MaterialTheme.colorScheme.background 48 | ) { 49 | val navControllerScreen = rememberNavController() 50 | val viewModel: MainViewModel = viewModel() 51 | 52 | NavHost( 53 | navController = navControllerScreen, 54 | startDestination = ScreenManager.SplashScreen.route, 55 | builder = { 56 | composable(route = ScreenManager.SplashScreen.route) { 57 | SplashScreen( 58 | navControllerScreen = navControllerScreen, 59 | viewModel = viewModel 60 | ) 61 | } 62 | composable(route = ScreenManager.MainScreen.route) { 63 | MainScreen( 64 | navControllerScreen = navControllerScreen, 65 | viewModel = viewModel 66 | ) 67 | } 68 | composable(route = ScreenManager.DetailsScreen.route) { 69 | DetailsScreen( 70 | navControllerScreen = navControllerScreen, 71 | viewModel = viewModel 72 | ) 73 | } 74 | composable(route = ScreenManager.LoginScreen.route) { 75 | LoginScreen( 76 | navControllerScreen = navControllerScreen, 77 | viewModel = viewModel 78 | ) 79 | } 80 | } 81 | ) 82 | } 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/pager/MessagePager.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.pager 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.foundation.shape.CircleShape 12 | import androidx.compose.material3.CircularProgressIndicator 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.LaunchedEffect 16 | import androidx.compose.runtime.collectAsState 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.clip 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import androidx.constraintlayout.compose.ConstraintLayout 28 | import androidx.constraintlayout.compose.Dimension 29 | import androidx.paging.compose.LazyPagingItems 30 | import androidx.paging.compose.collectAsLazyPagingItems 31 | import androidx.paging.compose.items 32 | import coil.compose.AsyncImage 33 | import com.anpe.coolbbsyou.network.data.intent.MainIntent 34 | import com.anpe.coolbbsyou.network.data.model.nofitication.Data 35 | import com.anpe.coolbbsyou.network.data.state.NotificationState 36 | import com.anpe.coolbbsyou.ui.main.MainViewModel 37 | import com.anpe.coolbbsyou.util.Utils.Companion.richToString 38 | import com.anpe.coolbbsyou.util.Utils.Companion.timeStampInterval 39 | 40 | @Composable 41 | fun MessagePager(viewModel: MainViewModel) { 42 | val notificationState by viewModel.notificationState.collectAsState() 43 | 44 | val timeMillis = System.currentTimeMillis() 45 | 46 | LaunchedEffect(key1 = true, block = { 47 | viewModel.sendIntent(MainIntent.GetNotification) 48 | }) 49 | 50 | Box( 51 | modifier = Modifier.fillMaxSize() 52 | ) { 53 | var dataList by remember { 54 | mutableStateOf?>(null) 55 | } 56 | 57 | when (notificationState) { 58 | is NotificationState.Error -> { 59 | Text(modifier = Modifier.align(Alignment.Center), text = (notificationState as NotificationState.Error).e) 60 | } 61 | NotificationState.Idle -> { 62 | Text(modifier = Modifier.align(Alignment.Center), text = "加载失败!") 63 | } 64 | NotificationState.Loading -> { 65 | CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) 66 | } 67 | is NotificationState.Success -> { 68 | dataList = (notificationState as NotificationState.Success).pager.collectAsLazyPagingItems() 69 | } 70 | } 71 | 72 | LazyColumn(content = { 73 | item { 74 | Column { 75 | BlockItem(title = "我的动态") 76 | BlockItem(title = "我的评论") 77 | BlockItem(title = "我收到的赞") 78 | BlockItem(title = "好友关注") 79 | BlockItem(title = "私信") 80 | } 81 | } 82 | 83 | dataList?.apply { 84 | items(this) { 85 | it?.apply { 86 | NotificationItem( 87 | fromUserAvatar, 88 | fromusername, 89 | note, 90 | (dateline.toLong() * 1000).timeStampInterval(timeMillis) 91 | ) 92 | } 93 | } 94 | } 95 | }) 96 | } 97 | } 98 | 99 | @Composable 100 | private fun BlockItem( 101 | modifier: Modifier = Modifier, 102 | title: String, 103 | tip: String? = null, 104 | ) { 105 | Box( 106 | modifier = modifier 107 | .fillMaxWidth() 108 | .padding(15.dp) 109 | .height(30.dp) 110 | ) { 111 | Column(modifier = Modifier.align(Alignment.CenterStart)) { 112 | Text(text = title, fontSize = 18.sp, fontWeight = FontWeight.Bold) 113 | tip?.apply { 114 | Text(text = this, fontSize = 14.sp) 115 | } 116 | } 117 | } 118 | } 119 | 120 | @Composable 121 | private fun NotificationItem(userAvatar: String, username: String, node: String, time: String) { 122 | ConstraintLayout( 123 | modifier = Modifier 124 | .fillMaxWidth() 125 | .padding(15.dp, 10.dp, 15.dp, 10.dp) 126 | ) { 127 | val (avatarRef, nameRef, messageRef, timeRef) = createRefs() 128 | 129 | AsyncImage( 130 | modifier = Modifier 131 | .size(45.dp) 132 | .clip(CircleShape) 133 | .constrainAs(avatarRef) { 134 | start.linkTo(parent.start) 135 | top.linkTo(parent.top) 136 | }, 137 | model = userAvatar, 138 | contentDescription = null 139 | ) 140 | 141 | Text( 142 | modifier = Modifier 143 | .constrainAs(nameRef) { 144 | start.linkTo(avatarRef.end, 10.dp) 145 | top.linkTo(avatarRef.top) 146 | }, 147 | text = username 148 | ) 149 | 150 | Text( 151 | modifier = Modifier 152 | .constrainAs(messageRef) { 153 | start.linkTo(nameRef.start) 154 | top.linkTo(nameRef.bottom) 155 | end.linkTo(parent.end) 156 | width = Dimension.fillToConstraints 157 | }, 158 | text = node.richToString(), 159 | fontSize = 13.sp, 160 | lineHeight = 17.sp 161 | ) 162 | 163 | Text( 164 | modifier = Modifier 165 | .constrainAs(timeRef) { 166 | end.linkTo(parent.end) 167 | top.linkTo(parent.top) 168 | }, 169 | text = time, 170 | fontSize = 11.sp 171 | ) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/pager/SettingsPager.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.pager 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material3.Switch 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.collectAsState 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.rememberCoroutineScope 18 | import androidx.compose.runtime.setValue 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import androidx.lifecycle.viewmodel.compose.viewModel 25 | import com.anpe.coolbbsyou.network.data.intent.MainIntent 26 | import com.anpe.coolbbsyou.network.data.state.IndexImageState 27 | import com.anpe.coolbbsyou.ui.main.MainViewModel 28 | import kotlinx.coroutines.launch 29 | 30 | @Composable 31 | fun SettingsPager(viewModel: MainViewModel = viewModel()) { 32 | val scope = rememberCoroutineScope() 33 | 34 | val t by viewModel.indexImageState.collectAsState() 35 | 36 | val isNineGrid = when (t) { 37 | IndexImageState.ImageRow -> false 38 | IndexImageState.NineGrid -> true 39 | } 40 | 41 | Column(Modifier.fillMaxSize()) { 42 | var checked by remember { 43 | mutableStateOf(isNineGrid) 44 | } 45 | 46 | SettingsSwitchItem( 47 | title = "首页图片九宫格", 48 | tip = "首页图片内容是否开启九宫格排版", 49 | checked = checked, 50 | onCheckedChange = { 51 | checked = it 52 | 53 | scope.launch { 54 | viewModel.channel.send(MainIntent.OpenNineGrid(checked)) 55 | } 56 | } 57 | ) 58 | } 59 | } 60 | 61 | @Composable 62 | private fun SettingsSwitchItem( 63 | modifier: Modifier = Modifier, 64 | title: String, 65 | tip: String? = null, 66 | checked: Boolean, 67 | onCheckedChange: ((Boolean) -> Unit) 68 | ) { 69 | Box( 70 | modifier = modifier 71 | .clickable { 72 | onCheckedChange(!checked) 73 | } 74 | .fillMaxWidth() 75 | .padding(15.dp) 76 | .height(50.dp) 77 | ) { 78 | Column(modifier = Modifier.align(Alignment.CenterStart)) { 79 | Text(text = title, fontSize = 18.sp, fontWeight = FontWeight.Bold) 80 | tip?.apply { 81 | Text(text = this, fontSize = 14.sp) 82 | } 83 | } 84 | Switch( 85 | modifier = Modifier 86 | .align(Alignment.CenterEnd), 87 | checked = checked, 88 | onCheckedChange = { 89 | onCheckedChange(it) 90 | } 91 | ) 92 | } 93 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/pager/TodayCoolPager.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.pager 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.PaddingValues 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.aspectRatio 9 | import androidx.compose.foundation.layout.fillMaxHeight 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.foundation.layout.width 15 | import androidx.compose.foundation.lazy.LazyColumn 16 | import androidx.compose.foundation.lazy.LazyRow 17 | import androidx.compose.foundation.lazy.items 18 | import androidx.compose.foundation.shape.RoundedCornerShape 19 | import androidx.compose.material.ExperimentalMaterialApi 20 | import androidx.compose.material.IconButton 21 | import androidx.compose.material.icons.Icons 22 | import androidx.compose.material.icons.filled.ArrowBack 23 | import androidx.compose.material.pullrefresh.PullRefreshIndicator 24 | import androidx.compose.material.pullrefresh.pullRefresh 25 | import androidx.compose.material.pullrefresh.rememberPullRefreshState 26 | import androidx.compose.material3.Card 27 | import androidx.compose.material3.CircularProgressIndicator 28 | import androidx.compose.material3.Icon 29 | import androidx.compose.material3.MaterialTheme 30 | import androidx.compose.material3.Text 31 | import androidx.compose.runtime.Composable 32 | import androidx.compose.runtime.collectAsState 33 | import androidx.compose.runtime.getValue 34 | import androidx.compose.runtime.mutableStateOf 35 | import androidx.compose.runtime.remember 36 | import androidx.compose.runtime.rememberCoroutineScope 37 | import androidx.compose.runtime.setValue 38 | import androidx.compose.ui.Alignment 39 | import androidx.compose.ui.Modifier 40 | import androidx.compose.ui.draw.clip 41 | import androidx.compose.ui.layout.ContentScale 42 | import androidx.compose.ui.platform.LocalConfiguration 43 | import androidx.compose.ui.platform.LocalContext 44 | import androidx.compose.ui.text.font.FontStyle 45 | import androidx.compose.ui.text.font.FontWeight 46 | import androidx.compose.ui.text.style.TextOverflow 47 | import androidx.compose.ui.unit.dp 48 | import androidx.compose.ui.unit.sp 49 | import androidx.navigation.NavHostController 50 | import coil.compose.AsyncImage 51 | import coil.request.ImageRequest 52 | import com.anpe.coolbbsyou.network.data.intent.MainIntent 53 | import com.anpe.coolbbsyou.network.data.model.today.Data 54 | import com.anpe.coolbbsyou.network.data.state.TodayState 55 | import com.anpe.coolbbsyou.ui.main.MainViewModel 56 | import com.anpe.coolbbsyou.util.Utils.Companion.clickableNoRipple 57 | import com.anpe.coolbbsyou.util.Utils.Companion.isTable 58 | import com.anpe.coolbbsyou.util.Utils.Companion.richToString 59 | import com.anpe.coolbbsyou.util.Utils.Companion.timeStampInterval 60 | import kotlinx.coroutines.launch 61 | 62 | @OptIn(ExperimentalMaterialApi::class) 63 | @Composable 64 | fun TodayCoolPager(navControllerInnerScreen: NavHostController, navControllerPager: NavHostController, viewModel: MainViewModel) { 65 | val scope = rememberCoroutineScope() 66 | val configuration = LocalConfiguration.current 67 | 68 | val todayState by viewModel.todayState.collectAsState() 69 | 70 | var refreshing by remember { 71 | mutableStateOf(false) 72 | } 73 | val refreshState = rememberPullRefreshState(refreshing = refreshing, onRefresh = { 74 | scope.launch { 75 | refreshing = true 76 | /*url?.apply { 77 | viewModel.channel.send(MainIntent.GetTodayCool(url = this, page = 1)) 78 | }*/ 79 | } 80 | }) 81 | 82 | Box(Modifier.fillMaxSize()) { 83 | val list: List = listOf() 84 | var dataList by remember { 85 | mutableStateOf(list) 86 | } 87 | 88 | when (todayState) { 89 | is TodayState.Error -> { 90 | refreshing = false 91 | val error = (todayState as TodayState.Error).e 92 | Text(modifier = Modifier.align(Alignment.Center), text = error) 93 | } 94 | TodayState.Idle -> { 95 | refreshing = false 96 | Text(modifier = Modifier.align(Alignment.Center), text = "idle") 97 | } 98 | TodayState.Loading -> { 99 | if (dataList.isEmpty()) { 100 | CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) 101 | } 102 | } 103 | is TodayState.Success -> { 104 | refreshing = false 105 | dataList = (todayState as TodayState.Success).todayCoolEntity.data 106 | } 107 | } 108 | 109 | Column { 110 | Row( 111 | modifier = Modifier 112 | .fillMaxWidth() 113 | .padding(top = 5.dp, bottom = 5.dp), 114 | verticalAlignment = Alignment.CenterVertically 115 | ) { 116 | IconButton( 117 | modifier = Modifier 118 | .padding(start = 15.dp), 119 | onClick = { 120 | navControllerPager.popBackStack() 121 | }) { 122 | Icon( 123 | imageVector = Icons.Default.ArrowBack, 124 | contentDescription = "ArrowBack" 125 | ) 126 | } 127 | 128 | Text( 129 | modifier = Modifier, 130 | text = "今日酷安", 131 | fontSize = 25.sp, 132 | fontWeight = FontWeight.Bold, 133 | fontStyle = FontStyle.Italic, 134 | color = MaterialTheme.colorScheme.primary 135 | ) 136 | } 137 | LazyColumn( 138 | modifier = Modifier 139 | .pullRefresh(refreshState) 140 | .fillMaxHeight(), 141 | contentPadding = PaddingValues(15.dp, 0.dp, 15.dp, 10.dp), 142 | content = { 143 | items(items = dataList) { 144 | IndexItems( 145 | modifier = Modifier.padding(top = 5.dp, bottom = 5.dp), 146 | data = it, 147 | onClick = { 148 | scope.launch { 149 | viewModel.channel.send(MainIntent.GetDetails(it.id)) 150 | if (!configuration.isTable()) { 151 | // navControllerInnerScreen.navigate(InnerScreenManager.DetailsInnerScreen.route) 152 | } 153 | } 154 | } 155 | ) 156 | } 157 | } 158 | ) 159 | } 160 | 161 | PullRefreshIndicator( 162 | modifier = Modifier.align(Alignment.TopCenter), 163 | refreshing = refreshing, 164 | state = refreshState, 165 | contentColor = MaterialTheme.colorScheme.surfaceTint 166 | ) 167 | } 168 | } 169 | 170 | @Composable 171 | private fun IndexItems( 172 | modifier: Modifier = Modifier, 173 | data: Data, 174 | onClick: () -> Unit 175 | ) { 176 | when (data.entityType) { 177 | "feed" -> { 178 | FeedItem(modifier = modifier, data = data, onClick = onClick) 179 | } 180 | 181 | "imageTextGridCard" -> { 182 | ImageTextItem(modifier = modifier, data = data, onClick = onClick) 183 | } 184 | } 185 | } 186 | 187 | @Composable 188 | private fun FeedItem( 189 | modifier: Modifier = Modifier, 190 | data: Data, 191 | onClick: () -> Unit 192 | ) { 193 | val context = LocalContext.current 194 | 195 | val timeMillis = System.currentTimeMillis() 196 | 197 | Card( 198 | modifier = modifier 199 | .clickableNoRipple { 200 | onClick() 201 | } 202 | .fillMaxWidth(), 203 | shape = RoundedCornerShape(15.dp) 204 | ) { 205 | Text( 206 | modifier = Modifier 207 | .padding(start = 10.dp, top = 10.dp, end = 10.dp), 208 | text = data.message.richToString() 209 | ) 210 | 211 | if (data.picArr.isNotEmpty()) { 212 | LazyRow( 213 | modifier = Modifier 214 | .padding(start = 10.dp, top = 10.dp, end = 10.dp), 215 | content = { 216 | for ((index, string) in data.picArr.withIndex()) { 217 | item { 218 | AsyncImage( 219 | modifier = Modifier 220 | .size(100.dp) 221 | .padding( 222 | start = if (index == 0) 0.dp else 2.dp, 223 | end = if (index == data.picArr.size - 1) 0.dp else 2.dp 224 | ) 225 | .clip(RoundedCornerShape(10.dp)) 226 | .aspectRatio(1f), 227 | model = ImageRequest.Builder(context) 228 | .data(string) 229 | .size(500) 230 | .build(), 231 | contentScale = ContentScale.Crop, 232 | contentDescription = "image" 233 | ) 234 | } 235 | } 236 | } 237 | ) 238 | } 239 | 240 | Text( 241 | modifier = Modifier 242 | .padding(start = 10.dp, top = 10.dp, end = 10.dp, bottom = 10.dp), 243 | text = "${data.username} ${data.replynum}评论 ${(data.createTime.toLong() * 1000).timeStampInterval(timeMillis)}", 244 | fontSize = 13.sp 245 | ) 246 | } 247 | } 248 | 249 | @Composable 250 | private fun ImageTextItem(modifier: Modifier = Modifier, data: Data, onClick: () -> Unit) { 251 | val context = LocalContext.current 252 | 253 | Column(modifier = modifier) { 254 | LazyRow(content = { 255 | items(data.entities) { 256 | Column( 257 | modifier = Modifier 258 | .width(180.dp) 259 | .padding( 260 | end = 10.dp 261 | ) 262 | .clip(RoundedCornerShape(15.dp)) 263 | .clickableNoRipple { 264 | onClick() 265 | } 266 | ) { 267 | AsyncImage( 268 | modifier = Modifier 269 | .fillMaxWidth() 270 | .aspectRatio(2f), 271 | model = ImageRequest.Builder(context) 272 | .data(it.pic) 273 | .build(), 274 | contentScale = ContentScale.Crop, 275 | contentDescription = "image" 276 | ) 277 | Text( 278 | modifier = Modifier 279 | .fillMaxWidth() 280 | .background(MaterialTheme.colorScheme.primaryContainer) 281 | .padding(5.dp), 282 | text = it.title, 283 | overflow = TextOverflow.Ellipsis, 284 | maxLines = 2, 285 | minLines = 2, 286 | fontSize = 15.sp 287 | ) 288 | } 289 | } 290 | }) 291 | } 292 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/pager/manager/PagerManager.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.pager.manager 2 | 3 | import androidx.annotation.StringRes 4 | import com.anpe.coolbbsyou.R 5 | 6 | sealed class PagerManager(val route: String, @StringRes val resourceId: Int) { 7 | object HomePager: PagerManager("HomePager", R.string.home_pager) 8 | object MessagePager: PagerManager("MessagePager", R.string.message_pager) 9 | object SettingsPager: PagerManager("SettingsPager", R.string.settings_pager) 10 | object TodayCoolPager: PagerManager("TodayCoolPager", R.string.settings_pager) 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/screen/DetailsScreen.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.screen 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.navigation.NavController 5 | import com.anpe.coolbbsyou.ui.main.MainViewModel 6 | import com.anpe.coolbbsyou.ui.view.DetailsPagerBridge 7 | 8 | @Composable 9 | fun DetailsScreen(navControllerScreen: NavController, viewModel: MainViewModel) { 10 | /*val details: DetailsEntity? = null 11 | var detailsEntity by remember { 12 | mutableStateOf(details) 13 | } 14 | 15 | if (LocalConfiguration.current.screenWidthDp >= 800) { 16 | navControllerScreen.popBackStack() 17 | } 18 | 19 | LaunchedEffect(key1 = true, block = { 20 | *//*id?.apply { 21 | viewModel.channel.send(MainIntent.GetDetails(this)) 22 | }*//* 23 | 24 | viewModel.detailsState.collect { 25 | when (it) { 26 | is DetailsState.Error -> {} 27 | DetailsState.Idle -> {} 28 | DetailsState.Loading -> {} 29 | is DetailsState.Success -> detailsEntity = it.detailsEntity 30 | } 31 | } 32 | }) 33 | 34 | detailsEntity?.apply { 35 | DetailsPager(entity = this, onBack = { 36 | navControllerScreen.popBackStack() 37 | }) 38 | }*/ 39 | 40 | DetailsPagerBridge(viewModel = viewModel, onBack = { 41 | navControllerScreen.popBackStack() 42 | }) 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/screen/LoginScreen.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.screen 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.ArrowBack 9 | import androidx.compose.material3.Button 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.IconButton 13 | import androidx.compose.material3.OutlinedTextField 14 | import androidx.compose.material3.Scaffold 15 | import androidx.compose.material3.Text 16 | import androidx.compose.material3.TopAppBar 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.LaunchedEffect 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.rememberCoroutineScope 23 | import androidx.compose.runtime.setValue 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.platform.LocalContext 27 | import androidx.compose.ui.text.input.TextFieldValue 28 | import androidx.compose.ui.unit.dp 29 | import androidx.lifecycle.viewmodel.compose.viewModel 30 | import androidx.navigation.NavHostController 31 | import com.anpe.coolbbsyou.network.data.intent.MainIntent 32 | import com.anpe.coolbbsyou.network.data.repository.ApiRepository 33 | import com.anpe.coolbbsyou.network.data.state.LoginState 34 | import com.anpe.coolbbsyou.ui.main.MainViewModel 35 | import com.anpe.coolbbsyou.util.LoginUtils.Companion.createRequestHash 36 | import com.anpe.coolbbsyou.util.ToastUtils.Companion.showToast 37 | import kotlinx.coroutines.launch 38 | import okhttp3.ResponseBody 39 | import org.jsoup.Jsoup 40 | import retrofit2.Call 41 | import retrofit2.Callback 42 | import retrofit2.Response 43 | 44 | @OptIn(ExperimentalMaterial3Api::class) 45 | @Composable 46 | fun LoginScreen(navControllerScreen: NavHostController, viewModel: MainViewModel) { 47 | Scaffold( 48 | topBar = { 49 | TopAppBar(title = { Text(text = "登陆") }, navigationIcon = { 50 | IconButton(onClick = { navControllerScreen.popBackStack() }) { 51 | Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null) 52 | } 53 | }) 54 | } 55 | ) { 56 | Column(Modifier.padding(it).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { 57 | Content(navControllerScreen, viewModel) 58 | } 59 | } 60 | } 61 | 62 | @Composable 63 | private fun Content(navControllerScreen: NavHostController, viewModel: MainViewModel = viewModel()) { 64 | val scope = rememberCoroutineScope() 65 | val context = LocalContext.current 66 | 67 | val TAG = "LoginScreen" 68 | 69 | val repository = ApiRepository() 70 | 71 | var requestHash by remember { 72 | mutableStateOf("") 73 | } 74 | 75 | var account by remember { 76 | mutableStateOf(TextFieldValue()) 77 | } 78 | var passwd by remember { 79 | mutableStateOf(TextFieldValue()) 80 | } 81 | 82 | LaunchedEffect(key1 = true, block = { 83 | repository.getRequestHash().enqueue(object : Callback { 84 | override fun onResponse( 85 | call: Call, 86 | response: Response 87 | ) { 88 | val body = response.body()?.string() 89 | body?.apply { 90 | requestHash = Jsoup.parse(this).createRequestHash() 91 | } 92 | } 93 | 94 | override fun onFailure(call: Call, t: Throwable) { 95 | } 96 | 97 | }) 98 | 99 | viewModel.loginState.collect { 100 | when (it) { 101 | is LoginState.Error -> { 102 | it.error.showToast() 103 | } 104 | LoginState.Idle -> {} 105 | LoginState.LoggingIn -> { 106 | Toast.makeText(context, "登陆中,请稍后", Toast.LENGTH_SHORT).show() 107 | } 108 | is LoginState.Success -> { 109 | if (it.loginEntity.status == 1) { 110 | navControllerScreen.popBackStack() 111 | } 112 | } 113 | } 114 | } 115 | }) 116 | 117 | 118 | OutlinedTextField( 119 | modifier = Modifier.padding(5.dp), 120 | label = { Text(text = "Account") }, 121 | value = account, 122 | onValueChange = { account = it } 123 | ) 124 | 125 | OutlinedTextField( 126 | modifier = Modifier.padding(5.dp), 127 | label = { Text(text = "Password") }, 128 | value = passwd, 129 | onValueChange = { passwd = it } 130 | ) 131 | 132 | Button(onClick = { 133 | scope.launch { 134 | if (requestHash.isNotEmpty()) { 135 | viewModel.channel.send(MainIntent.LoginAccount(account = account.text, passwd = passwd.text, requestHash = requestHash, captcha = "")) 136 | } 137 | } 138 | }) { 139 | Text(text = "登陆") 140 | } 141 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/screen/SplashScreen.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.screen 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.LaunchedEffect 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.alpha 19 | import androidx.compose.ui.platform.LocalContext 20 | import androidx.compose.ui.unit.dp 21 | import androidx.navigation.NavController 22 | import coil.compose.AsyncImage 23 | import coil.request.ImageRequest 24 | import com.anpe.coolbbsyou.R 25 | import com.anpe.coolbbsyou.network.data.intent.MainIntent 26 | import com.anpe.coolbbsyou.ui.main.MainViewModel 27 | import com.anpe.coolbbsyou.ui.screen.manager.ScreenManager 28 | import kotlinx.coroutines.delay 29 | 30 | @Composable 31 | fun SplashScreen(navControllerScreen: NavController, viewModel: MainViewModel) { 32 | var startAnimation by remember { 33 | mutableStateOf(false) 34 | } 35 | 36 | val alphaAnim = animateFloatAsState( 37 | targetValue = if (startAnimation) 1f else 0f, 38 | animationSpec = tween(durationMillis = 500), label = "" 39 | ) 40 | 41 | LaunchedEffect(key1 = true) { 42 | startAnimation = true 43 | delay(1000) 44 | navControllerScreen.popBackStack() 45 | navControllerScreen.navigate(ScreenManager.MainScreen.route) 46 | } 47 | 48 | LaunchedEffect(key1 = true) { 49 | viewModel.sendIntent(MainIntent.LoginState) 50 | } 51 | 52 | Splash(alphaAnim.value) 53 | } 54 | 55 | @Composable 56 | private fun Splash(alpha: Float) { 57 | val context = LocalContext.current 58 | 59 | Box( 60 | modifier = Modifier 61 | .background(MaterialTheme.colorScheme.background) 62 | .fillMaxSize(), 63 | contentAlignment = Alignment.Center 64 | ) { 65 | AsyncImage( 66 | modifier = Modifier 67 | .size(100.dp) 68 | .alpha(alpha), 69 | model = ImageRequest.Builder(context) 70 | .data(R.mipmap.ic_launcher) 71 | .crossfade(true) 72 | .build(), 73 | contentDescription = "logo" 74 | ) 75 | } 76 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/screen/manager/ScreenManager.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.screen.manager 2 | 3 | import androidx.annotation.StringRes 4 | import com.anpe.coolbbsyou.R 5 | 6 | sealed class ScreenManager(val route: String, @StringRes val resourceId: Int) { 7 | object SplashScreen: ScreenManager("SplashScreen", R.string.splash_screen) 8 | 9 | object MainScreen: ScreenManager("MainScreen", R.string.main_screen) 10 | 11 | object LoginScreen: ScreenManager("LoginScreen", R.string.login_screen) 12 | 13 | object DetailsScreen: ScreenManager("DetailsInnerScreen", R.string.details_screen) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.toArgb 15 | import androidx.compose.ui.platform.LocalContext 16 | import androidx.compose.ui.platform.LocalView 17 | import androidx.core.view.WindowCompat 18 | 19 | private val DarkColorScheme = darkColorScheme( 20 | primary = Purple80, 21 | secondary = PurpleGrey80, 22 | tertiary = Pink80 23 | ) 24 | 25 | private val LightColorScheme = lightColorScheme( 26 | primary = Purple40, 27 | secondary = PurpleGrey40, 28 | tertiary = Pink40 29 | 30 | /* Other default colors to override 31 | background = Color(0xFFFFFBFE), 32 | surface = Color(0xFFFFFBFE), 33 | onPrimary = Color.White, 34 | onSecondary = Color.White, 35 | onTertiary = Color.White, 36 | onBackground = Color(0xFF1C1B1F), 37 | onSurface = Color(0xFF1C1B1F), 38 | */ 39 | ) 40 | 41 | @Composable 42 | fun CoolbbsYouTheme( 43 | darkTheme: Boolean = isSystemInDarkTheme(), 44 | // Dynamic color is available on Android 12+ 45 | dynamicColor: Boolean = true, 46 | content: @Composable () -> Unit 47 | ) { 48 | val colorScheme = when { 49 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 50 | val context = LocalContext.current 51 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 52 | } 53 | 54 | darkTheme -> DarkColorScheme 55 | else -> LightColorScheme 56 | } 57 | val view = LocalView.current 58 | if (!view.isInEditMode) { 59 | SideEffect { 60 | val window = (view.context as Activity).window 61 | window.statusBarColor = Color.Transparent.toArgb() 62 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme 63 | } 64 | } 65 | 66 | MaterialTheme( 67 | colorScheme = colorScheme, 68 | typography = Typography, 69 | content = content 70 | ) 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.theme 2 | 3 | import androidx.compose.material3.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 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/BottomSheetDialog.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.bilibiliandyou.ui.view 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.compose.animation.AnimatedVisibility 5 | import androidx.compose.animation.core.LinearEasing 6 | import androidx.compose.animation.core.LinearOutSlowInEasing 7 | import androidx.compose.animation.core.animateFloatAsState 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.animation.fadeIn 10 | import androidx.compose.animation.fadeOut 11 | import androidx.compose.animation.slideInVertically 12 | import androidx.compose.animation.slideOutVertically 13 | import androidx.compose.foundation.background 14 | import androidx.compose.foundation.clickable 15 | import androidx.compose.foundation.gestures.Orientation 16 | import androidx.compose.foundation.gestures.draggable 17 | import androidx.compose.foundation.gestures.rememberDraggableState 18 | import androidx.compose.foundation.interaction.MutableInteractionSource 19 | import androidx.compose.foundation.layout.Box 20 | import androidx.compose.foundation.layout.BoxScope 21 | import androidx.compose.foundation.layout.fillMaxSize 22 | import androidx.compose.foundation.layout.imePadding 23 | import androidx.compose.foundation.layout.offset 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.DisposableEffect 26 | import androidx.compose.runtime.getValue 27 | import androidx.compose.runtime.mutableStateOf 28 | import androidx.compose.runtime.remember 29 | import androidx.compose.runtime.setValue 30 | import androidx.compose.ui.Alignment 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.composed 33 | import androidx.compose.ui.graphics.Color 34 | import androidx.compose.ui.layout.onGloballyPositioned 35 | import androidx.compose.ui.unit.IntOffset 36 | import kotlin.math.roundToInt 37 | 38 | @Composable 39 | fun BottomSheetDialog( 40 | modifier: Modifier = Modifier, 41 | visible: Boolean, 42 | cancelable: Boolean = true, 43 | canceledOnTouchOutside: Boolean = true, 44 | onDismissRequest: () -> Unit, 45 | content: @Composable () -> Unit 46 | ) { 47 | BackHandler( 48 | enabled = visible, 49 | onBack = { 50 | if (cancelable) { 51 | onDismissRequest() 52 | } 53 | } 54 | ) 55 | 56 | Box(modifier = modifier) { 57 | AnimatedVisibility( 58 | visible = visible, 59 | enter = fadeIn(animationSpec = tween(durationMillis = 400, easing = LinearEasing)), 60 | exit = fadeOut(animationSpec = tween(durationMillis = 400, easing = LinearEasing)) 61 | ) { 62 | Box( 63 | modifier = Modifier 64 | .fillMaxSize() 65 | .background(color = Color(0x99000000)) 66 | .clickableNoRipple { 67 | if (canceledOnTouchOutside) { 68 | onDismissRequest() 69 | } 70 | } 71 | ) 72 | } 73 | 74 | InnerDialog( 75 | visible = visible, 76 | cancelable = cancelable, 77 | onDismissRequest = onDismissRequest, 78 | content = content 79 | ) 80 | } 81 | } 82 | 83 | @Composable 84 | private fun BoxScope.InnerDialog( 85 | visible: Boolean, 86 | cancelable: Boolean, 87 | onDismissRequest: () -> Unit, 88 | content: @Composable () -> Unit 89 | ) { 90 | var offsetY by remember { 91 | mutableStateOf(0f) 92 | } 93 | val offsetYAnimate by animateFloatAsState(targetValue = offsetY) 94 | var bottomSheetHeight by remember { mutableStateOf(0f) } 95 | 96 | AnimatedVisibility( 97 | modifier = Modifier 98 | .clickableNoRipple { 99 | 100 | } 101 | .imePadding() 102 | .align(alignment = Alignment.BottomCenter) 103 | .onGloballyPositioned { 104 | bottomSheetHeight = it.size.height.toFloat() 105 | } 106 | .offset { 107 | IntOffset(0, offsetYAnimate.roundToInt()) 108 | } 109 | .draggable( 110 | state = rememberDraggableState( 111 | onDelta = { 112 | offsetY = (offsetY + it.toInt()).coerceAtLeast(0f) 113 | } 114 | ), 115 | orientation = Orientation.Vertical, 116 | onDragStarted = { 117 | 118 | }, 119 | onDragStopped = { 120 | if (cancelable && offsetY > bottomSheetHeight / 2) { 121 | onDismissRequest() 122 | } else { 123 | offsetY = 0f 124 | } 125 | } 126 | ), 127 | visible = visible, 128 | enter = slideInVertically( 129 | animationSpec = tween(durationMillis = 400, easing = LinearOutSlowInEasing), 130 | initialOffsetY = { 2 * it } 131 | ), 132 | exit = slideOutVertically( 133 | animationSpec = tween(durationMillis = 400, easing = LinearOutSlowInEasing), 134 | targetOffsetY = { it } 135 | ), 136 | ) { 137 | DisposableEffect(key1 = null) { 138 | onDispose { 139 | offsetY = 0f 140 | } 141 | } 142 | content() 143 | } 144 | } 145 | 146 | private fun Modifier.clickableNoRipple(onClick: () -> Unit): Modifier = 147 | composed { 148 | clickable( 149 | indication = null, 150 | interactionSource = remember { MutableInteractionSource() } 151 | ) { 152 | onClick() 153 | } 154 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/CustomProgress.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.view 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.foundation.Canvas 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.setValue 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.geometry.Offset 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.StrokeCap 14 | 15 | @Composable 16 | fun CustomProgress( 17 | modifier: Modifier = Modifier, 18 | currentValue: Int, 19 | maxValue: Int, 20 | strokeWidth: Float = 20f, 21 | primaryColor: Color = Color.White, 22 | secondaryColor: Color = Color.Gray 23 | ) { 24 | var maxWidth by remember { 25 | mutableStateOf(1f) 26 | } 27 | 28 | val currentWidth by animateFloatAsState( 29 | targetValue = if (maxValue == 0 && currentValue == 0) 30 | 0f 31 | else 32 | maxWidth * (currentValue / maxValue.toFloat()), 33 | label = "" 34 | ) 35 | 36 | Canvas( 37 | modifier = modifier 38 | ) { 39 | // 画布的高 40 | val canvasHeight = size.height 41 | 42 | // 画布的宽 43 | val canvasWidth = size.width 44 | 45 | maxWidth = canvasWidth 46 | 47 | drawLine( 48 | color = secondaryColor, 49 | start = Offset(0f, 0f), 50 | end = Offset(canvasWidth, 0f), 51 | strokeWidth = strokeWidth, 52 | cap = StrokeCap.Round 53 | ) 54 | 55 | drawLine( 56 | color = primaryColor, 57 | start = Offset(0f, 0f), 58 | end = Offset(currentWidth, 0f), 59 | strokeWidth = strokeWidth, 60 | cap = StrokeCap.Round 61 | ) 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/DetailsPagerBridge.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.view 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.material3.CircularProgressIndicator 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.collectAsState 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.res.painterResource 21 | import androidx.compose.ui.text.font.FontStyle 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.unit.sp 25 | import com.anpe.coolbbsyou.R 26 | import com.anpe.coolbbsyou.network.data.state.DetailsState 27 | import com.anpe.coolbbsyou.ui.main.MainViewModel 28 | import com.anpe.coolbbsyou.ui.pager.DetailsPager 29 | 30 | 31 | @Composable 32 | fun DetailsPagerBridge(viewModel: MainViewModel, onBack: () -> Unit) { 33 | val detailsState by viewModel.detailsState.collectAsState() 34 | 35 | Box( 36 | modifier = Modifier 37 | .fillMaxSize() 38 | .background(Color(if (isSystemInDarkTheme()) 0xff0d0d0d else 0xfff5f5f5)) 39 | ) { 40 | when (detailsState) { 41 | is DetailsState.Error -> { 42 | Text( 43 | modifier = Modifier.align(Alignment.Center), 44 | text = (detailsState as DetailsState.Error).e 45 | ) 46 | } 47 | 48 | is DetailsState.Idle -> { 49 | Row( 50 | modifier = Modifier 51 | .align(Alignment.Center) 52 | .padding(10.dp), 53 | verticalAlignment = Alignment.CenterVertically 54 | ) { 55 | Icon( 56 | modifier = Modifier 57 | .size(100.dp), 58 | painter = painterResource(id = R.drawable.coolapk), 59 | contentDescription = "icon", 60 | tint = Color(if (isSystemInDarkTheme()) 0xff161616 else 0xfff1f1f1) 61 | ) 62 | Text( 63 | text = "Coolbbs", 64 | fontWeight = FontWeight.Bold, 65 | fontSize = 55.sp, 66 | color = Color(if (isSystemInDarkTheme()) 0xff161616 else 0xfff1f1f1), 67 | fontStyle = FontStyle.Italic 68 | ) 69 | } 70 | } 71 | 72 | is DetailsState.Loading -> { 73 | CircularProgressIndicator( 74 | modifier = Modifier.align(Alignment.Center) 75 | ) 76 | } 77 | 78 | is DetailsState.Success -> { 79 | val detailsEntity = (detailsState as DetailsState.Success).detailsEntity 80 | 81 | DetailsPager( 82 | modifier = Modifier.fillMaxWidth(), 83 | entity = detailsEntity, 84 | onBack = onBack 85 | ) 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/FullScreenImage.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.view 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.gestures.detectDragGestures 5 | import androidx.compose.foundation.gestures.detectTapGestures 6 | import androidx.compose.foundation.gestures.rememberTransformableState 7 | import androidx.compose.foundation.gestures.transformable 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.pager.HorizontalPager 12 | import androidx.compose.foundation.pager.rememberPagerState 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.material3.Surface 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.clip 23 | import androidx.compose.ui.geometry.Offset 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.graphics.graphicsLayer 26 | import androidx.compose.ui.input.pointer.pointerInput 27 | import androidx.compose.ui.platform.LocalContext 28 | import androidx.compose.ui.unit.dp 29 | import androidx.compose.ui.window.Dialog 30 | import coil.compose.AsyncImage 31 | import coil.request.ImageRequest 32 | import com.anpe.coolbbsyou.util.Utils.Companion.clickableNoRipple 33 | 34 | @Composable 35 | fun FullScreenImage( 36 | modifier: Modifier = Modifier, 37 | url: String, 38 | onClick: () -> Unit = {} 39 | ) { 40 | var scale by remember { mutableStateOf(1f) } 41 | var offset by remember { mutableStateOf(Offset.Zero) } 42 | val state = rememberTransformableState { zoomChange, _, _ -> 43 | scale = (zoomChange * scale).coerceAtLeast(1f) 44 | } 45 | 46 | Surface( 47 | color = Color.DarkGray, 48 | modifier = modifier 49 | .fillMaxSize() 50 | .pointerInput(Unit) { 51 | detectTapGestures( 52 | onDoubleTap = { 53 | scale = 1f 54 | offset = Offset.Zero 55 | }, 56 | onTap = { 57 | onClick.invoke() 58 | } 59 | ) 60 | } 61 | ) { 62 | AsyncImage( 63 | modifier = Modifier 64 | .fillMaxSize() 65 | .transformable(state = state) 66 | .graphicsLayer( 67 | scaleX = scale, 68 | scaleY = scale, 69 | translationX = offset.x, 70 | translationY = offset.y 71 | ) 72 | .pointerInput(Unit) { 73 | detectDragGestures { _, dragAmount -> 74 | offset += dragAmount 75 | } 76 | }, 77 | model = url, 78 | contentDescription = "" 79 | ) 80 | } 81 | } 82 | 83 | @OptIn(ExperimentalFoundationApi::class) 84 | @Composable 85 | fun DialogImage(picList: List, initialPage: Int, onDismissRequest: () -> Unit,) { 86 | val pagerState = rememberPagerState( 87 | initialPage = initialPage, 88 | ) 89 | Dialog(onDismissRequest = { onDismissRequest() }) { 90 | Box(Modifier.fillMaxSize().clickableNoRipple { onDismissRequest() }) { 91 | HorizontalPager( 92 | modifier = Modifier.align(Alignment.Center) 93 | .clip(RoundedCornerShape(15.dp)), 94 | pageCount = picList.size, 95 | state = pagerState, 96 | verticalAlignment = Alignment.CenterVertically, 97 | pageSpacing = 10.dp 98 | ) { 99 | AsyncImage( 100 | modifier = Modifier 101 | .fillMaxWidth() 102 | .clip(RoundedCornerShape(15.dp)), 103 | model = ImageRequest.Builder(LocalContext.current) 104 | .data(picList[it]) 105 | .build(), 106 | contentDescription = null 107 | ) 108 | } 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/LoadableLazyColumn.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.view 2 | 3 | import androidx.compose.foundation.gestures.FlingBehavior 4 | import androidx.compose.foundation.gestures.ScrollableDefaults 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.PaddingValues 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.foundation.lazy.LazyListScope 12 | import androidx.compose.foundation.lazy.LazyListState 13 | import androidx.compose.foundation.lazy.rememberLazyListState 14 | import androidx.compose.material3.CircularProgressIndicator 15 | import androidx.compose.material.ExperimentalMaterialApi 16 | import androidx.compose.material.pullrefresh.PullRefreshDefaults 17 | import androidx.compose.material.pullrefresh.PullRefreshIndicator 18 | import androidx.compose.material.pullrefresh.PullRefreshState 19 | import androidx.compose.material.pullrefresh.pullRefresh 20 | import androidx.compose.material.pullrefresh.rememberPullRefreshState 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.LaunchedEffect 23 | import androidx.compose.runtime.derivedStateOf 24 | import androidx.compose.runtime.getValue 25 | import androidx.compose.runtime.mutableStateOf 26 | import androidx.compose.runtime.remember 27 | import androidx.compose.runtime.setValue 28 | import androidx.compose.ui.Alignment 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.unit.Dp 31 | import androidx.compose.ui.unit.dp 32 | 33 | @OptIn(ExperimentalMaterialApi::class) 34 | @Composable 35 | fun LoadableLazyColumn( 36 | modifier: Modifier = Modifier, 37 | state: LoadableLazyColumnState, 38 | refreshing: Boolean, 39 | loading: Boolean, 40 | contentPadding: PaddingValues = PaddingValues(0.dp), 41 | reverseLayout: Boolean = false, 42 | verticalArrangement: Arrangement.Vertical = 43 | if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, 44 | horizontalAlignment: Alignment.Horizontal = Alignment.Start, 45 | flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), 46 | userScrollEnabled: Boolean = true, 47 | loadingContent: (@Composable () -> Unit)? = null, 48 | content: LazyListScope.() -> Unit, 49 | ) { 50 | val lazyListState = state.lazyListState 51 | // 获取 lazyList 布局信息 52 | val listLayoutInfo by remember { derivedStateOf { lazyListState.layoutInfo } } 53 | Box( 54 | modifier = modifier 55 | .pullRefresh(state.pullRefreshState) 56 | ) { 57 | LazyColumn( 58 | contentPadding = contentPadding, 59 | state = state.lazyListState, 60 | reverseLayout = reverseLayout, 61 | verticalArrangement = verticalArrangement, 62 | horizontalAlignment = horizontalAlignment, 63 | flingBehavior = flingBehavior, 64 | userScrollEnabled = userScrollEnabled, 65 | content = { 66 | content() 67 | item { 68 | if (loadingContent != null) { 69 | loadingContent() 70 | } else { 71 | if (loading) { 72 | Box(modifier = Modifier.fillMaxWidth()) { 73 | CircularProgressIndicator( 74 | modifier = Modifier 75 | .size(30.dp) 76 | .align(Alignment.Center) 77 | ) 78 | } 79 | } 80 | } 81 | } 82 | }, 83 | ) 84 | PullRefreshIndicator( 85 | refreshing, 86 | state.pullRefreshState, 87 | Modifier.align(Alignment.TopCenter) 88 | ) 89 | } 90 | // 上次是否正在滑动 91 | var lastTimeIsScrollInProgress by remember { 92 | mutableStateOf(lazyListState.isScrollInProgress) 93 | } 94 | // 上次滑动结束后最后一个可见的index 95 | var lastTimeLastVisibleIndex by remember { 96 | mutableStateOf(listLayoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) 97 | } 98 | // 当前是否正在滑动 99 | val currentIsScrollInProgress = lazyListState.isScrollInProgress 100 | // 当前最后一个可见的 index 101 | val currentLastVisibleIndex = listLayoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 102 | if (!currentIsScrollInProgress && lastTimeIsScrollInProgress) { 103 | if (currentLastVisibleIndex != lastTimeLastVisibleIndex) { 104 | val isScrollDown = currentLastVisibleIndex > lastTimeLastVisibleIndex 105 | val remainCount = listLayoutInfo.totalItemsCount - currentLastVisibleIndex - 1 106 | if (isScrollDown && remainCount <= state.loadMoreState.loadMoreRemainCountThreshold) { 107 | LaunchedEffect(Unit) { 108 | state.loadMoreState.onLoadMore() 109 | } 110 | } 111 | } 112 | // 滑动结束后再更新值 113 | lastTimeLastVisibleIndex = currentLastVisibleIndex 114 | } 115 | lastTimeIsScrollInProgress = currentIsScrollInProgress 116 | } 117 | 118 | @Composable 119 | @ExperimentalMaterialApi 120 | fun rememberLoadableLazyColumnState( 121 | refreshing: Boolean, 122 | onRefresh: () -> Unit, 123 | onLoadMore: () -> Unit, 124 | refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold, 125 | refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset, 126 | loadMoreRemainCountThreshold: Int = 5, 127 | initialFirstVisibleItemIndex: Int = 0, 128 | initialFirstVisibleItemScrollOffset: Int = 0 129 | ): LoadableLazyColumnState { 130 | val pullRefreshState = rememberPullRefreshState( 131 | refreshing = refreshing, 132 | onRefresh = onRefresh, 133 | refreshingOffset = refreshingOffset, 134 | refreshThreshold = refreshThreshold, 135 | ) 136 | 137 | val lazyListState = rememberLazyListState( 138 | initialFirstVisibleItemScrollOffset = initialFirstVisibleItemScrollOffset, 139 | initialFirstVisibleItemIndex = initialFirstVisibleItemIndex, 140 | ) 141 | 142 | val loadMoreState = rememberLoadMoreState(loadMoreRemainCountThreshold, onLoadMore) 143 | 144 | return remember(pullRefreshState, lazyListState, loadMoreState) { 145 | LoadableLazyColumnState( 146 | lazyListState = lazyListState, 147 | pullRefreshState = pullRefreshState, 148 | loadMoreState = loadMoreState, 149 | ) 150 | } 151 | } 152 | 153 | @Composable 154 | fun rememberLoadMoreState( 155 | loadMoreRemainCountThreshold: Int, 156 | onLoadMore: () -> Unit 157 | ): LoadMoreState { 158 | return remember { 159 | LoadMoreState(loadMoreRemainCountThreshold, onLoadMore) 160 | } 161 | } 162 | 163 | @OptIn(ExperimentalMaterialApi::class) 164 | data class LoadableLazyColumnState( 165 | val lazyListState: LazyListState, 166 | val pullRefreshState: PullRefreshState, 167 | val loadMoreState: LoadMoreState, 168 | ) 169 | 170 | data class LoadMoreState( 171 | val loadMoreRemainCountThreshold: Int, 172 | val onLoadMore: () -> Unit 173 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/MyDialog.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.bilibiliandyou.ui.view 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.unit.dp 13 | import androidx.compose.ui.unit.sp 14 | import androidx.compose.ui.window.Dialog 15 | 16 | @Composable 17 | fun MyDialog( 18 | title: String, 19 | onDismissRequest: () -> Unit, 20 | dismissButton: @Composable () -> Unit, 21 | deleteButton: (@Composable () -> Unit)?, 22 | updateButton: (@Composable () -> Unit)?, 23 | content: @Composable () -> Unit, 24 | ) { 25 | Dialog(onDismissRequest = { onDismissRequest() }) { 26 | Column( 27 | modifier = Modifier 28 | .width(300.dp) 29 | .clip(RoundedCornerShape(30.dp)) 30 | .background(MaterialTheme.colorScheme.surfaceVariant), 31 | ) { 32 | Column(modifier = Modifier.padding(24.dp)) { 33 | Column(modifier = Modifier.padding(bottom = 16.dp)) { 34 | Text( 35 | text = title, 36 | fontSize = 24.sp 37 | ) 38 | } 39 | 40 | Column( 41 | modifier = Modifier 42 | .fillMaxWidth() 43 | ) { 44 | content() 45 | } 46 | 47 | Row( 48 | modifier = Modifier 49 | .padding(top = 25.dp) 50 | .fillMaxWidth() 51 | .align(Alignment.End), 52 | verticalAlignment = Alignment.CenterVertically, 53 | horizontalArrangement = Arrangement.End 54 | ) { 55 | deleteButton?.let { 56 | Box { 57 | deleteButton() 58 | } 59 | } 60 | Box(modifier = Modifier.padding(start = 5.dp)) { 61 | dismissButton() 62 | } 63 | updateButton?.let { 64 | Box(modifier = Modifier.padding(start = 5.dp)) { 65 | updateButton() 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/MyLabel.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.bilibiliandyou.ui.view 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.LocalContentColor 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.res.painterResource 19 | import androidx.compose.ui.unit.Dp 20 | import androidx.compose.ui.unit.TextUnit 21 | import androidx.compose.ui.unit.dp 22 | import androidx.compose.ui.unit.sp 23 | 24 | 25 | @Composable 26 | fun MyLabel( 27 | modifier: Modifier = Modifier, 28 | // margin: PaddingValues = PaddingValues(0.dp), 29 | contentMargin: PaddingValues = PaddingValues(5.dp, 3.dp, 5.dp, 3.dp), 30 | backgroundColor: Color = MaterialTheme.colorScheme.primaryContainer, 31 | radio: Dp = 5.dp, 32 | text: String, 33 | fontSize: TextUnit = 13.sp, 34 | icon: Int? = null, 35 | iconTint: Color = LocalContentColor.current, 36 | iconSize: Dp = 15.dp, 37 | iconMargin: PaddingValues = PaddingValues(end = 3.dp) 38 | ) { 39 | Row( 40 | modifier = modifier 41 | // .padding(margin) 42 | .clip(RoundedCornerShape(radio)) 43 | .background(backgroundColor) 44 | .padding(contentMargin), 45 | verticalAlignment = Alignment.CenterVertically 46 | ) { 47 | icon?.let { 48 | Icon( 49 | modifier = Modifier.size(iconSize).padding(iconMargin), 50 | painter = painterResource(id = it), 51 | tint = iconTint, 52 | contentDescription = "" 53 | ) 54 | } 55 | Text( 56 | modifier = Modifier, 57 | text = text, 58 | fontSize = fontSize 59 | ) 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/MyScaffolWithDetails.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.view 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.width 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Scaffold 9 | import androidx.compose.material3.Surface 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.platform.LocalConfiguration 13 | import androidx.compose.ui.unit.Dp 14 | import androidx.compose.ui.unit.dp 15 | 16 | 17 | @Composable 18 | fun MyScaffoldWithDetails( 19 | modifier: Modifier = Modifier, 20 | changeValue: Dp = 800.dp, 21 | topBar: @Composable () -> Unit = {}, 22 | bottomBar: @Composable () -> Unit = {}, 23 | railBar: (@Composable () -> Unit)? = null, 24 | detailsBlock: (@Composable () -> Unit) ? = null, 25 | floatingActionButton: @Composable () -> Unit = {}, 26 | content: @Composable (PaddingValues) -> Unit, 27 | ) { 28 | Surface( 29 | modifier = Modifier.fillMaxSize(), 30 | color = MaterialTheme.colorScheme.background 31 | ) { 32 | val configuration = LocalConfiguration.current 33 | 34 | val changeState = configuration.screenWidthDp.dp >= changeValue 35 | 36 | // val isDetails = detailsBlock != null 37 | 38 | Row(modifier = Modifier.fillMaxSize()) { 39 | if (railBar != null) { 40 | if (changeState) { 41 | railBar() 42 | } 43 | } 44 | 45 | Scaffold( 46 | modifier = if (changeState) modifier.width(400.dp) else modifier, 47 | topBar = { 48 | topBar() 49 | }, 50 | content = { 51 | content(it) 52 | }, 53 | floatingActionButton = { 54 | floatingActionButton() 55 | }, 56 | bottomBar = { 57 | if (railBar == null) { 58 | bottomBar() 59 | } else { 60 | if (!changeState) { 61 | bottomBar() 62 | } 63 | } 64 | } 65 | ) 66 | 67 | if (detailsBlock != null && changeState) { 68 | detailsBlock() 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/MyScaffold.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.view 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.PaddingValues 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Scaffold 12 | import androidx.compose.material3.Surface 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.platform.LocalConfiguration 16 | import androidx.compose.ui.unit.Dp 17 | import androidx.compose.ui.unit.dp 18 | import com.anpe.bilibiliandyou.ui.view.BottomSheetDialog 19 | 20 | @Composable 21 | fun MyScaffold( 22 | modifier: Modifier = Modifier, 23 | changeValue: Dp = 800.dp, 24 | topBar: @Composable () -> Unit = {}, 25 | bottomBar: @Composable () -> Unit = {}, 26 | railBar: (@Composable () -> Unit)? = null, 27 | floatingActionButton: @Composable () -> Unit = {}, 28 | content: @Composable (PaddingValues) -> Unit, 29 | ) { 30 | Surface( 31 | modifier = Modifier.fillMaxSize(), 32 | color = MaterialTheme.colorScheme.background 33 | ) { 34 | val configuration = LocalConfiguration.current 35 | 36 | Row(modifier = Modifier.fillMaxSize()) { 37 | if (railBar != null) { 38 | if (configuration.screenWidthDp.dp >= changeValue) { 39 | railBar() 40 | } 41 | } 42 | Scaffold( 43 | modifier = modifier, 44 | topBar = { 45 | topBar() 46 | }, 47 | content = { 48 | content(it) 49 | }, 50 | floatingActionButton = { 51 | floatingActionButton() 52 | }, 53 | bottomBar = { 54 | if (railBar == null) { 55 | bottomBar() 56 | } else { 57 | if (configuration.screenWidthDp.dp < changeValue) { 58 | bottomBar() 59 | } 60 | } 61 | } 62 | ) 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/MySheetScaffold.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.view 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Scaffold 11 | import androidx.compose.material3.Surface 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.unit.Dp 15 | import androidx.compose.ui.unit.dp 16 | import com.anpe.bilibiliandyou.ui.view.BottomSheetDialog 17 | 18 | @Composable 19 | fun MySheetScaffold( 20 | modifier: Modifier = Modifier, 21 | topBar: @Composable () -> Unit = {}, 22 | bottomBar: @Composable () -> Unit = {}, 23 | railBar: @Composable () -> Unit = {}, 24 | sheetContent: @Composable () -> Unit, 25 | floatingActionButton: @Composable () -> Unit = {}, 26 | configuration: Configuration, 27 | changeValue: Dp? = null, 28 | visible: Boolean, 29 | cancelable: Boolean = true, 30 | canceledOnTouchOutside: Boolean = true, 31 | onDismissRequest: () -> Unit, 32 | content: @Composable () -> Unit, 33 | ) { 34 | Surface( 35 | modifier = Modifier.fillMaxSize(), 36 | color = MaterialTheme.colorScheme.background 37 | ) { 38 | Box(modifier = Modifier.fillMaxSize()) { 39 | Row { 40 | if (changeValue != null) { 41 | if (configuration.screenWidthDp.dp >= changeValue) { 42 | railBar() 43 | } 44 | } 45 | Scaffold( 46 | modifier = modifier, 47 | topBar = { 48 | topBar() 49 | }, 50 | content = { 51 | Column( 52 | modifier = Modifier.padding( 53 | top = it.calculateTopPadding(), 54 | bottom = it.calculateBottomPadding() 55 | ) 56 | ) { 57 | content() 58 | } 59 | }, 60 | floatingActionButton = { 61 | floatingActionButton() 62 | }, 63 | bottomBar = { 64 | if (changeValue == null) { 65 | bottomBar() 66 | } else { 67 | if (configuration.screenWidthDp.dp < changeValue) { 68 | bottomBar() 69 | } 70 | } 71 | } 72 | ) 73 | } 74 | 75 | BottomSheetDialog( 76 | modifier = Modifier, 77 | visible = visible, 78 | cancelable = cancelable, 79 | canceledOnTouchOutside = canceledOnTouchOutside, 80 | onDismissRequest = onDismissRequest, 81 | content = { 82 | sheetContent() 83 | } 84 | ) 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/MyTest.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.view 2 | 3 | import androidx.compose.material.ExperimentalMaterialApi 4 | import androidx.compose.runtime.Composable 5 | 6 | @OptIn(ExperimentalMaterialApi::class) 7 | @Composable 8 | fun MyTest(entity: Any) { 9 | /*var refreshing by remember { 10 | mutableStateOf(false) 11 | } 12 | val refreshState = rememberPullRefreshState(refreshing = refreshing, onRefresh = { 13 | scope.launch { 14 | refreshing = true 15 | viewModel.channel.send(MainIntent.GetIndex) 16 | } 17 | }) 18 | 19 | Box(Modifier.fillMaxSize()) { 20 | val list: List = listOf() 21 | var dataList by remember { 22 | mutableStateOf(list) 23 | } 24 | 25 | when (todayState) { 26 | is TodayState.Error -> { 27 | refreshing = false 28 | val error = (todayState as TodayState.Error).e 29 | Text(modifier = Modifier.align(Alignment.Center), text = error) 30 | } 31 | TodayState.Idle -> { 32 | 33 | refreshing = false 34 | Text(modifier = Modifier.align(Alignment.Center), text = "idle") 35 | 36 | } 37 | TodayState.Loading -> { 38 | 39 | if (dataList.isEmpty()) { 40 | CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) 41 | } 42 | 43 | } 44 | is TodayState.Success -> { 45 | 46 | 47 | refreshing = false 48 | dataList = (todayState as TodayState.Success).todayCoolEntity.data 49 | } 50 | } 51 | 52 | LazyColumn( 53 | modifier = Modifier 54 | .pullRefresh(refreshState) 55 | .fillMaxHeight(), 56 | contentPadding = PaddingValues(15.dp, 0.dp, 15.dp, 10.dp), 57 | content = { 58 | items(items = dataList) { 59 | } 60 | } 61 | ) 62 | 63 | PullRefreshIndicator( 64 | modifier = Modifier.align(Alignment.TopCenter), 65 | refreshing = refreshing, 66 | state = refreshState, 67 | contentColor = MaterialTheme.colorScheme.surfaceTint 68 | ) 69 | }*/ 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/NineImageGrid.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.view 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.aspectRatio 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.clip 13 | import androidx.compose.ui.graphics.Shape 14 | import androidx.compose.ui.layout.ContentScale 15 | import androidx.compose.ui.platform.LocalContext 16 | import androidx.compose.ui.unit.dp 17 | import coil.compose.AsyncImage 18 | import coil.request.ImageRequest 19 | import com.anpe.coolbbsyou.util.Utils.Companion.clickableNoRipple 20 | 21 | @Composable 22 | fun NineImageGrid( 23 | modifier: Modifier = Modifier, 24 | list: List, 25 | itemPadding: PaddingValues = PaddingValues(0.dp), 26 | itemClip: Shape = RoundedCornerShape(0.dp), 27 | onClick: (Int) -> Unit 28 | ) { 29 | val context = LocalContext.current 30 | 31 | Column(modifier = modifier) { 32 | val size = list.size 33 | 34 | val cell = if (size <= 3) { 35 | 0 36 | } else if (size <= 6) { 37 | 1 38 | } else { 39 | 2 40 | } 41 | 42 | for (index in 0..cell) { 43 | Row { 44 | for (j in (0 + index * 3)..(2 + index * 3)) { 45 | if (j < size) { 46 | AsyncImage( 47 | modifier = Modifier 48 | .padding(itemPadding) 49 | .clip(itemClip) 50 | .weight(1f) 51 | .aspectRatio(1f) 52 | .clickableNoRipple { 53 | onClick(j) 54 | }, 55 | model = ImageRequest.Builder(context) 56 | .data(list[j]) 57 | .size(500) 58 | .crossfade(true) 59 | .build(), 60 | contentScale = ContentScale.Crop, 61 | contentDescription = "image" 62 | ) 63 | } else { 64 | Spacer(modifier = Modifier.weight(1f)) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/ResponsiveLayout.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.ui.view 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.width 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Surface 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.platform.LocalConfiguration 12 | import androidx.compose.ui.unit.Dp 13 | import androidx.compose.ui.unit.dp 14 | 15 | 16 | @Composable 17 | fun ResponsiveLayout( 18 | modifier: Modifier = Modifier, 19 | changeValue: Dp = 800.dp, 20 | // topBar: @Composable () -> Unit = {}, 21 | // bottomBar: @Composable () -> Unit = {}, 22 | railBar: (@Composable () -> Unit)? = null, 23 | detailsBlock: (@Composable () -> Unit) ? = null, 24 | // floatingActionButton: @Composable () -> Unit = {}, 25 | content: @Composable () -> Unit, 26 | ) { 27 | Surface( 28 | modifier = Modifier.fillMaxSize(), 29 | color = MaterialTheme.colorScheme.background 30 | ) { 31 | val configuration = LocalConfiguration.current 32 | 33 | val changeState = configuration.screenWidthDp.dp >= changeValue 34 | 35 | Row(modifier = Modifier.fillMaxSize()) { 36 | if (railBar != null) { 37 | if (changeState) { 38 | railBar() 39 | } 40 | } 41 | 42 | Column( 43 | modifier = if (changeState) modifier.width(400.dp) else modifier, 44 | content = { 45 | content() 46 | } 47 | ) 48 | 49 | if (detailsBlock != null && changeState) { 50 | detailsBlock() 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/ui/view/TextIcon.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.bilibiliandyou.ui.view 2 | 3 | import androidx.compose.material3.Icon 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.res.painterResource 10 | import androidx.compose.ui.unit.TextUnit 11 | import androidx.compose.ui.unit.dp 12 | import androidx.compose.ui.unit.sp 13 | import androidx.constraintlayout.compose.ConstraintLayout 14 | 15 | 16 | @Composable 17 | fun TextIcon( 18 | modifier: Modifier = Modifier, 19 | iconId: Int, 20 | iconTint: Color = MaterialTheme.colorScheme.surfaceTint, 21 | textDirection: TextDirection = TextDirection.Bottom, 22 | text: String? = null, 23 | fontSize: TextUnit = 10.sp, 24 | contentDescription: String? = null 25 | ) { 26 | ConstraintLayout(modifier = modifier) { 27 | val (iconRef, textRef) = createRefs() 28 | 29 | Icon( 30 | modifier = Modifier.constrainAs(iconRef) { 31 | start.linkTo(parent.start) 32 | top.linkTo(parent.top) 33 | end.linkTo(parent.end) 34 | bottom.linkTo(parent.bottom) 35 | }, 36 | painter = painterResource(id = iconId), 37 | tint = iconTint, 38 | contentDescription = contentDescription, 39 | ) 40 | 41 | text?.let { 42 | Text( 43 | modifier = Modifier.constrainAs(textRef) { 44 | when (textDirection) { 45 | TextDirection.Bottom -> { 46 | top.linkTo(iconRef.bottom, 5.dp) 47 | start.linkTo(parent.start) 48 | end.linkTo(parent.end) 49 | } 50 | TextDirection.End -> { 51 | start.linkTo(iconRef.end, 5.dp) 52 | top.linkTo(parent.top) 53 | bottom.linkTo(parent.bottom) 54 | } 55 | TextDirection.Start -> { 56 | end.linkTo(iconRef.start, 5.dp) 57 | top.linkTo(parent.top) 58 | bottom.linkTo(parent.bottom) 59 | } 60 | TextDirection.Top -> { 61 | bottom.linkTo(iconRef.top, 5.dp) 62 | start.linkTo(parent.start) 63 | end.linkTo(parent.end) 64 | } 65 | } 66 | }, 67 | text = text, 68 | fontSize = fontSize 69 | ) 70 | } 71 | } 72 | } 73 | 74 | sealed class TextDirection { 75 | object Start : TextDirection() 76 | object Top : TextDirection() 77 | object End : TextDirection() 78 | object Bottom : TextDirection() 79 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/util/LoginUtils.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.util 2 | 3 | import org.jsoup.nodes.Document 4 | 5 | class LoginUtils { 6 | companion object { 7 | fun Document.createRequestHash() = this.getElementsByTag("Body").attr("data-request-hash") 8 | 9 | fun createRandomNumber() = Math.random().toString().replace(".", "undefined") 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/util/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.content.Context 6 | 7 | class MyApplication: Application() { 8 | companion object { 9 | @SuppressLint("StaticFieldLeak") 10 | lateinit var context: Context 11 | } 12 | 13 | override fun onCreate() { 14 | super.onCreate() 15 | context = applicationContext 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/util/SharedPreferencesUtils.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.util 2 | 3 | import android.content.Context 4 | 5 | class SharedPreferencesUtils { 6 | companion object { 7 | private fun baseSP(context: Context, content: String, mode: Int) = 8 | context.getSharedPreferences(content, mode) 9 | 10 | private fun edit(context: Context, content: String, mode: Int) = 11 | baseSP(context, content, mode).edit() 12 | 13 | private fun get(context: Context, content: String, mode: Int) = 14 | baseSP(context, content, mode) 15 | 16 | fun String.put2SP( 17 | key: String, 18 | content: String = MyApplication.context.packageName, 19 | context: Context = MyApplication.context, 20 | mode: Int = Context.MODE_PRIVATE 21 | ) = edit(context, content, mode).putString(key, this) 22 | 23 | fun Int.put2SP( 24 | key: String, 25 | content: String = MyApplication.context.packageName, 26 | context: Context = MyApplication.context, 27 | mode: Int = Context.MODE_PRIVATE 28 | ) = edit(context, content, mode).putInt(key, this) 29 | 30 | fun Boolean.put2SP( 31 | key: String, 32 | content: String = MyApplication.context.packageName, 33 | context: Context = MyApplication.context, 34 | mode: Int = Context.MODE_PRIVATE 35 | ) = edit(context, content, mode).putBoolean(key, this) 36 | 37 | fun Float.put2SP( 38 | key: String, 39 | content: String = MyApplication.context.packageName, 40 | context: Context = MyApplication.context, 41 | mode: Int = Context.MODE_PRIVATE 42 | ) = edit(context, content, mode).putFloat(key, this) 43 | 44 | fun Long.put2SP( 45 | key: String, 46 | content: String = MyApplication.context.packageName, 47 | context: Context = MyApplication.context, 48 | mode: Int = Context.MODE_PRIVATE 49 | ) = edit(context, content, mode).putLong(key, this) 50 | 51 | fun getString( 52 | key: String, 53 | defValue: String? = null, 54 | content: String = MyApplication.context.packageName, 55 | context: Context = MyApplication.context, 56 | mode: Int = Context.MODE_PRIVATE 57 | ) = baseSP(context, content, mode).getString(key, defValue) 58 | 59 | fun getInt( 60 | key: String, 61 | defValue: Int = -1, 62 | content: String = MyApplication.context.packageName, 63 | context: Context = MyApplication.context, 64 | mode: Int = Context.MODE_PRIVATE 65 | ) = baseSP(context, content, mode).getInt(key, defValue) 66 | 67 | fun getLong( 68 | key: String, 69 | defValue: Long = -1L, 70 | content: String = MyApplication.context.packageName, 71 | context: Context = MyApplication.context, 72 | mode: Int = Context.MODE_PRIVATE 73 | ) = baseSP(context, content, mode).getLong(key, defValue) 74 | 75 | fun getFloat( 76 | key: String, 77 | defValue: Float = -1f, 78 | content: String = MyApplication.context.packageName, 79 | context: Context = MyApplication.context, 80 | mode: Int = Context.MODE_PRIVATE 81 | ) = baseSP(context, content, mode).getFloat(key, defValue) 82 | 83 | fun getBoolean( 84 | key: String, 85 | defValue: Boolean = false, 86 | content: String = MyApplication.context.packageName, 87 | context: Context = MyApplication.context, 88 | mode: Int = Context.MODE_PRIVATE 89 | ) = baseSP(context, content, mode).getBoolean(key, defValue) 90 | } 91 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/util/ToastUtils.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.util 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | 6 | class ToastUtils { 7 | companion object { 8 | private fun String.showToastBase( 9 | context: Context, 10 | duration: Int 11 | ) = Toast.makeText(context, this, duration).show() 12 | 13 | fun String.showToast( 14 | context: Context = MyApplication.context, 15 | duration: Int = Toast.LENGTH_SHORT 16 | ) = this.showToastBase(context, duration) 17 | 18 | fun Int.showToast( 19 | context: Context = MyApplication.context, 20 | duration: Int = Toast.LENGTH_SHORT 21 | ) = this.toString().showToastBase(context, duration) 22 | 23 | fun Boolean.showToast( 24 | context: Context = MyApplication.context, 25 | duration: Int = Toast.LENGTH_SHORT 26 | ) = this.toString().showToastBase(context, duration) 27 | 28 | fun Float.showToast( 29 | context: Context = MyApplication.context, 30 | duration: Int = Toast.LENGTH_SHORT 31 | ) = this.toString().showToastBase(context, duration) 32 | 33 | fun Double.showToast( 34 | context: Context = MyApplication.context, 35 | duration: Int = Toast.LENGTH_SHORT 36 | ) = this.toString().showToastBase(context, duration) 37 | 38 | fun showToastString( 39 | text: String, 40 | context: Context = MyApplication.context, 41 | duration: Int = Toast.LENGTH_SHORT 42 | ) = text.showToastBase(context, duration) 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/util/TokenDeviceUtils.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.util 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.provider.Settings 6 | import com.anpe.coolbbsyou.constant.Constants 7 | import com.anpe.coolbbsyou.network.data.model.deviceInfo.DeviceInfo 8 | import com.anpe.coolbbsyou.util.Utils.Companion.getBase64 9 | import com.anpe.coolbbsyou.util.Utils.Companion.getMD5 10 | 11 | class TokenDeviceUtils { 12 | companion object { 13 | private fun DeviceInfo.createDeviceCode(isRaw: Boolean = true) = 14 | "$aid; ; ; $mac; $manuFactor; $brand; $model; $buildNumber".getBase64(isRaw).reversed() 15 | 16 | private fun getDeviceCode(context: Context): String { 17 | val aid = Settings.System.getString(context.contentResolver, Settings.Secure.ANDROID_ID) 18 | val mac = Utils.randomMacAddress() 19 | val manuFactor = Build.MANUFACTURER 20 | val brand = Build.BRAND 21 | val model = Build.MODEL 22 | val buildNumber = "CoolbbsYou ${Build.VERSION.RELEASE}" 23 | 24 | return DeviceInfo(aid, mac, manuFactor, brand, model, buildNumber).createDeviceCode() 25 | } 26 | 27 | fun String.getTokenV2(): String { 28 | val timeStamp = (System.currentTimeMillis() / 1000f).toString() 29 | 30 | val base64TimeStamp = timeStamp.getBase64() 31 | val md5TimeStamp = timeStamp.getMD5() 32 | val md5DeviceCode = this.getMD5() 33 | 34 | val token = "${Constants.APP_LABEL}?$md5TimeStamp$$md5DeviceCode&${Constants.APP_ID}" 35 | val base64Token = token.getBase64() 36 | val md5Base64Token = base64Token.getMD5() 37 | val md5Token = token.getMD5() 38 | 39 | val bcryptSalt = "${"$2a$10$$base64TimeStamp/$md5Token".substring(0, 31)}u" 40 | val bcryptResult = org.mindrot.jbcrypt.BCrypt.hashpw(md5Base64Token, bcryptSalt) 41 | 42 | return "v2${bcryptResult.getBase64()}" 43 | } 44 | 45 | fun getLastingDeviceCode(context: Context): String { 46 | val sp = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) 47 | return sp.getString("DEVICE_CODE", null).let { 48 | it ?: getDeviceCode(context).apply { 49 | sp.edit().putString("DEVICE_CODE", this).apply() 50 | } 51 | } 52 | } 53 | 54 | fun getLastingInstallTime(context: Context): String { 55 | val sp = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) 56 | return sp.getString("INSTALL_TIME", null).let { 57 | it ?: System.currentTimeMillis().toString().apply { 58 | sp.edit().putString("INSTALL_TIME", this).apply() 59 | } 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anpe/coolbbsyou/util/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.anpe.coolbbsyou.util 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.composed 9 | import androidx.core.text.HtmlCompat 10 | import java.security.MessageDigest 11 | import java.text.SimpleDateFormat 12 | import java.util.Base64 13 | import java.util.Calendar 14 | import java.util.Date 15 | import java.util.Locale 16 | import java.util.Random 17 | 18 | 19 | class Utils { 20 | companion object { 21 | /** 22 | * 检测设备宽度是否大于等于800dp 23 | * 24 | * @receiver Configuration 25 | * @return boolean 26 | */ 27 | fun Configuration.isTable() = this.screenWidthDp >= 800 28 | 29 | /** 30 | * 二进制数组转十六进制字符串 31 | * 32 | * @receiver byte array to be converted 33 | * @return string containing hex values 34 | */ 35 | fun ByteArray.byteArrayToHexString(): String { 36 | val sb = StringBuilder(size * 2) 37 | for (element in this) { 38 | val v = element.toInt() and 0xff 39 | if (v < 16) { 40 | sb.append('0') 41 | } 42 | sb.append(Integer.toHexString(v)) 43 | } 44 | return sb.toString().uppercase(Locale.US) 45 | } 46 | 47 | /** 48 | * 十六进制字符串转二进制数组 49 | * 50 | * @receiver string of hex-encoded values 51 | * @return decoded byte array 52 | */ 53 | fun String.hexStringToByteArray(): ByteArray { 54 | val data = ByteArray(length / 2) 55 | for (index in indices step 2) { 56 | data[index / 2] = ( 57 | (Character.digit(this[index], 16) shl 4) + 58 | Character.digit(this[index + 1], 16) 59 | ).toByte() 60 | } 61 | return data 62 | } 63 | 64 | /** 65 | * 根据字符串生成Base64码 66 | * 67 | * @receiver 原生字符串 68 | * @param isRaw 是否省略“=”符号 69 | * @return Base64码 70 | */ 71 | fun String.getBase64(isRaw: Boolean = true): String { 72 | var result = Base64.getEncoder().encodeToString(this.toByteArray()) 73 | if (isRaw) { 74 | result = result.replace("=", "") 75 | } 76 | return result 77 | } 78 | 79 | /** 80 | * 根据字符串生成MD5值 81 | * 82 | * @receiver 原生字符串 83 | * @return MD5值 84 | */ 85 | fun String.getMD5(): String { 86 | val instance: MessageDigest = MessageDigest.getInstance("MD5") 87 | 88 | val digest = instance.digest(this.toByteArray()) 89 | 90 | val sb = StringBuffer() 91 | 92 | for (b in digest) { 93 | val i = b.toInt() and 0xff 94 | var hexString = Integer.toHexString(i) 95 | if (hexString.length < 2) { 96 | hexString = "0$hexString" 97 | } 98 | sb.append(hexString) 99 | } 100 | 101 | return sb.toString().replace("-", "") 102 | } 103 | 104 | /** 105 | * 随机生成Mac地址 106 | * 107 | * @return Mac地址 108 | */ 109 | fun randomMacAddress(): String { 110 | val random = Random() 111 | val sb = StringBuilder() 112 | 113 | for (i in 0..5) { 114 | if (sb.isNotEmpty()) { 115 | sb.append(":") 116 | } 117 | val value = random.nextInt(256) 118 | val element = Integer.toHexString(value) 119 | if (element.length < 2) { 120 | sb.append(0) 121 | } 122 | sb.append(element) 123 | } 124 | 125 | return sb.toString().uppercase() 126 | } 127 | 128 | /** 129 | * 拓展Modifier无点击效果点击方法 130 | * 131 | * @receiver Modifier 132 | * @return Modifier 133 | */ 134 | fun Modifier.clickableNoRipple(onClick: () -> Unit) = composed { 135 | clickable( 136 | indication = null, 137 | interactionSource = remember { MutableInteractionSource() } 138 | ) { 139 | onClick() 140 | } 141 | } 142 | 143 | /** 144 | * 格式化时间戳 145 | * 146 | * @receiver 时间戳 147 | * @return 格式化的时间 148 | */ 149 | fun Long.secondToDateString(pattern: String = "yyyy-MM-dd HH:mm:ss") = 150 | SimpleDateFormat(pattern, Locale.CHINA).format(Date(this)) 151 | 152 | /** 153 | * 计算时间差 154 | * 155 | * @receiver old timestamp 156 | * @param currentTime current timestamp 157 | */ 158 | fun Long.timeStampInterval(currentTime: Long): String { 159 | val calendarOld = Calendar.getInstance() 160 | val calendarNow = Calendar.getInstance() 161 | 162 | val dateOld = Date(this) 163 | val dateNow = Date(currentTime) 164 | 165 | calendarOld.time = dateOld 166 | calendarNow.time = dateNow 167 | 168 | val dayOld = calendarOld.get(Calendar.DAY_OF_YEAR) 169 | val dayNow = calendarNow.get(Calendar.DAY_OF_YEAR) 170 | 171 | val hourOld = calendarOld.get(Calendar.HOUR_OF_DAY) 172 | val hourNow = calendarNow.get(Calendar.HOUR_OF_DAY) 173 | 174 | val minuteOld = calendarOld.get(Calendar.MINUTE) 175 | val minuteNow = calendarNow.get(Calendar.MINUTE) 176 | 177 | return if (dayOld == dayNow) { 178 | if (hourOld == hourNow) { 179 | "${minuteNow - minuteOld}分钟前" 180 | } else { 181 | "${hourNow - hourOld}小时前" 182 | } 183 | } else { 184 | "${dayNow - dayOld}天前" 185 | } 186 | } 187 | 188 | /** 189 | * 格式化富文本 190 | * 191 | * @receiver 原始文本 192 | * @return 格式化后文本 193 | */ 194 | fun String.richToString(htmlCompat: Int = HtmlCompat.FROM_HTML_MODE_LEGACY) = 195 | HtmlCompat.fromHtml(this, htmlCompat).toString() 196 | } 197 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_chat_bubble_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_cloud_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_error_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_exit_to_app_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_hdr_strong_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_history_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_home_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_label_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_message_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_reply_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_settings_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_share_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_star_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_supervised_user_circle_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_thumb_up_alt_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/coolapk.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_user_avatar.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitFme/CoolbbsYou/871b5c84d04edb92e1c44abc5f3128461318563e/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitFme/CoolbbsYou/871b5c84d04edb92e1c44abc5f3128461318563e/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitFme/CoolbbsYou/871b5c84d04edb92e1c44abc5f3128461318563e/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitFme/CoolbbsYou/871b5c84d04edb92e1c44abc5f3128461318563e/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitFme/CoolbbsYou/871b5c84d04edb92e1c44abc5f3128461318563e/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitFme/CoolbbsYou/871b5c84d04edb92e1c44abc5f3128461318563e/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitFme/CoolbbsYou/871b5c84d04edb92e1c44abc5f3128461318563e/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitFme/CoolbbsYou/871b5c84d04edb92e1c44abc5f3128461318563e/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitFme/CoolbbsYou/871b5c84d04edb92e1c44abc5f3128461318563e/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitFme/CoolbbsYou/871b5c84d04edb92e1c44abc5f3128461318563e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-en/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CoolbbsYou 3 | 4 | Splash 5 | Main 6 | Login 7 | Details 8 | 9 | Home 10 | Today selection 11 | 12 | Home 13 | Message 14 | Settings 15 | 16 | follow 17 | fans 18 | feed 19 | CoolbbsYou 20 | -------------------------------------------------------------------------------- /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 | 4 | 闪屏 5 | 主屏 6 | 登陆 7 | 详情 8 | 9 | 主页 10 | 今日酷安 11 | 12 | 主页 13 | 消息 14 | 设置 15 | 16 | 关注 17 | 粉丝 18 | 动态 19 | 酷你基哇 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |