├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── eternaljust │ │ └── msea │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── html │ │ │ └── privacy.html │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── eternaljust │ │ │ └── msea │ │ │ ├── MainActivity.kt │ │ │ ├── MseaApp.kt │ │ │ ├── ui │ │ │ ├── data │ │ │ │ └── JSONModel.kt │ │ │ ├── page │ │ │ │ ├── home │ │ │ │ │ ├── HomePage.kt │ │ │ │ │ ├── HomeViewModel.kt │ │ │ │ │ ├── search │ │ │ │ │ │ ├── SearchPage.kt │ │ │ │ │ │ ├── SearchPostPage.kt │ │ │ │ │ │ ├── SearchPostViewModel.kt │ │ │ │ │ │ ├── SearchUserPage.kt │ │ │ │ │ │ ├── SearchUserViewModel.kt │ │ │ │ │ │ └── SearchViewModel.kt │ │ │ │ │ ├── sign │ │ │ │ │ │ ├── SignListPage.kt │ │ │ │ │ │ ├── SignListViewModel.kt │ │ │ │ │ │ ├── SignPage.kt │ │ │ │ │ │ └── SignViewModel.kt │ │ │ │ │ └── topic │ │ │ │ │ │ ├── TopicDetailPage.kt │ │ │ │ │ │ ├── TopicDetailViewModel.kt │ │ │ │ │ │ ├── TopicListPage.kt │ │ │ │ │ │ └── TopicListViewModel.kt │ │ │ │ ├── node │ │ │ │ │ ├── NodePage.kt │ │ │ │ │ ├── NodeViewModel.kt │ │ │ │ │ ├── list │ │ │ │ │ │ ├── NodeListPage.kt │ │ │ │ │ │ └── NodeListViewMode.kt │ │ │ │ │ └── tag │ │ │ │ │ │ ├── TagListPage.kt │ │ │ │ │ │ ├── TagListViewModel.kt │ │ │ │ │ │ ├── TagPage.kt │ │ │ │ │ │ └── TagViewModel.kt │ │ │ │ ├── notice │ │ │ │ │ ├── NoticePage.kt │ │ │ │ │ ├── NoticeViewModel.kt │ │ │ │ │ ├── interactive │ │ │ │ │ │ ├── InteractivePage.kt │ │ │ │ │ │ └── InteractiveViewModel.kt │ │ │ │ │ ├── post │ │ │ │ │ │ ├── MyPostPage.kt │ │ │ │ │ │ └── MyPostViewModel.kt │ │ │ │ │ └── system │ │ │ │ │ │ ├── SystemPage.kt │ │ │ │ │ │ └── SystemViewModel.kt │ │ │ │ └── profile │ │ │ │ │ ├── detail │ │ │ │ │ ├── ProfileCreditListPage.kt │ │ │ │ │ ├── ProfileCreditListViewModel.kt │ │ │ │ │ ├── ProfileCreditPage.kt │ │ │ │ │ ├── ProfileCreditViewModel.kt │ │ │ │ │ ├── ProfileDetailPage.kt │ │ │ │ │ ├── ProfileDetailViewModel.kt │ │ │ │ │ ├── ProfileFavoritePage.kt │ │ │ │ │ ├── ProfileFavoriteViewModel.kt │ │ │ │ │ ├── ProfileFirendViewModel.kt │ │ │ │ │ ├── ProfileFriendListPage.kt │ │ │ │ │ ├── ProfileFriendListViewModel.kt │ │ │ │ │ ├── ProfileFriendPage.kt │ │ │ │ │ ├── ProfileGroupPage.kt │ │ │ │ │ ├── ProfileTopicPage.kt │ │ │ │ │ └── ProfileTopicViewModel.kt │ │ │ │ │ ├── drawer │ │ │ │ │ ├── DrawerPage.kt │ │ │ │ │ └── DrawerViewModel.kt │ │ │ │ │ ├── login │ │ │ │ │ ├── LoginPage.kt │ │ │ │ │ └── LoginViewModel.kt │ │ │ │ │ └── setting │ │ │ │ │ ├── AboutPage.kt │ │ │ │ │ ├── AboutViewModel.kt │ │ │ │ │ ├── SettingPage.kt │ │ │ │ │ └── SettingViewModel.kt │ │ │ ├── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── widget │ │ │ │ ├── CommonWidget.kt │ │ │ │ ├── RefreshList.kt │ │ │ │ ├── TopAppBar.kt │ │ │ │ └── WebView.kt │ │ │ └── utils │ │ │ ├── Constants.kt │ │ │ ├── DataStoreUtil.kt │ │ │ ├── JsonUtil.kt │ │ │ ├── NetworkUtil.kt │ │ │ ├── Paging.kt │ │ │ ├── RouteUtil.kt │ │ │ ├── SettingCacheInfo.kt │ │ │ ├── StatisticsTool.kt │ │ │ └── UserCacheInfo.kt │ └── res │ │ ├── drawable │ │ ├── ic_baseline_admin_panel_settings_24.xml │ │ ├── ic_baseline_android_24.xml │ │ ├── ic_baseline_arrow_downward_24.xml │ │ ├── ic_baseline_arrow_drop_down_24.xml │ │ ├── ic_baseline_arrow_drop_up_24.xml │ │ ├── ic_baseline_arrow_forward_ios_24.xml │ │ ├── ic_baseline_arrow_upward_24.xml │ │ ├── ic_baseline_business_center_24.xml │ │ ├── ic_baseline_calendar_month_24.xml │ │ ├── ic_baseline_cloud_download_24.xml │ │ ├── ic_baseline_computer_24.xml │ │ ├── ic_baseline_contacts_24.xml │ │ ├── ic_baseline_currency_exchange_24.xml │ │ ├── ic_baseline_dark_mode_24.xml │ │ ├── ic_baseline_desktop_mac_24.xml │ │ ├── ic_baseline_elderly_24.xml │ │ ├── ic_baseline_energy_savings_leaf_24.xml │ │ ├── ic_baseline_event_available_24.xml │ │ ├── ic_baseline_feed_24.xml │ │ ├── ic_baseline_feedback_24.xml │ │ ├── ic_baseline_g_translate_24.xml │ │ ├── ic_baseline_grid_view_24.xml │ │ ├── ic_baseline_group_24.xml │ │ ├── ic_baseline_help_24.xml │ │ ├── ic_baseline_image_24.xml │ │ ├── ic_baseline_key_24.xml │ │ ├── ic_baseline_keyboard_double_arrow_right_24.xml │ │ ├── ic_baseline_laptop_24.xml │ │ ├── ic_baseline_light_24.xml │ │ ├── ic_baseline_lightbulb_24.xml │ │ ├── ic_baseline_link_24.xml │ │ ├── ic_baseline_local_fire_department_24.xml │ │ ├── ic_baseline_logout_24.xml │ │ ├── ic_baseline_man_24.xml │ │ ├── ic_baseline_paid_24.xml │ │ ├── ic_baseline_pin_24.xml │ │ ├── ic_baseline_privacy_tip_24.xml │ │ ├── ic_baseline_public_24.xml │ │ ├── ic_baseline_restaurant_24.xml │ │ ├── ic_baseline_school_24.xml │ │ ├── ic_baseline_settings_brightness_24.xml │ │ ├── ic_baseline_sms_24.xml │ │ ├── ic_baseline_sports_soccer_24.xml │ │ ├── ic_baseline_swap_horiz_24.xml │ │ ├── ic_baseline_tag_24.xml │ │ ├── ic_baseline_topic_24.xml │ │ ├── ic_baseline_view_list_24.xml │ │ ├── ic_baseline_visibility_24.xml │ │ ├── ic_baseline_visibility_off_24.xml │ │ ├── ic_baseline_waving_hand_24.xml │ │ ├── ic_baseline_wb_sunny_24.xml │ │ ├── ic_baseline_web_24.xml │ │ ├── ic_baseline_woman_24.xml │ │ ├── ic_baseline_workspace_premium_24.xml │ │ ├── ic_baseline_zoom_in_24.xml │ │ ├── ic_launcher.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── icon.png │ │ └── icon_avd.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── cloud_config_parms.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── eternaljust │ └── msea │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Mac files 6 | .DS_Store 7 | 8 | # files for the dex VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # generated files 15 | bin/ 16 | gen/ 17 | 18 | # Ignore gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | proguard-project.txt 28 | 29 | # Eclipse files 30 | .project 31 | .classpath 32 | .settings/ 33 | 34 | # Android Studio/IDEA 35 | *.iml 36 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Msea Compose 2 | 3 | 一款虫部落搜索论坛第三方 App,使用 Jetpack Compose 开发,采用最新的 Material Design 3 设计,支持 Android 10 及以上,适配 Material You 壁纸变化的配色方案,动态颜色适用于 Android 12 及更高版本。 4 | 5 | ![home](https://s1.ax1x.com/2023/01/16/pSl2Wsx.md.png) 6 | 7 | ![notice](https://s1.ax1x.com/2023/01/16/pSlRKfJ.md.png) 8 | 9 | ![node](https://s1.ax1x.com/2023/01/16/pSlRQp9.md.png) 10 | 11 | ![profile](https://s1.ax1x.com/2023/01/16/pSlRuY4.md.png) 12 | 13 | ![setting](https://s1.ax1x.com/2023/01/16/pSlRnkF.md.png) 14 | 15 | ![sign](https://s1.ax1x.com/2023/01/16/pSlReTU.md.png) 16 | 17 | ![topic](https://s1.ax1x.com/2023/01/16/pSlRllR.md.png) 18 | 19 | ![user](https://s1.ax1x.com/2023/01/16/pSlR161.md.png) 20 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'org.jetbrains.kotlin.plugin.compose' 5 | id 'kotlin-parcelize' 6 | } 7 | 8 | android { 9 | namespace 'com.eternaljust.msea' 10 | compileSdk 36 11 | 12 | defaultConfig { 13 | applicationId "com.eternaljust.msea" 14 | minSdk 29 15 | targetSdk 36 16 | versionCode 20 17 | versionName "1.1.1" 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | vectorDrawables { 21 | useSupportLibrary true 22 | } 23 | 24 | ndk { 25 | //noinspection ChromeOsAbiSupport 26 | abiFilters "arm64-v8a" 27 | } 28 | } 29 | 30 | buildTypes { 31 | release { 32 | // 混淆 33 | minifyEnabled true 34 | // 自动移除未使用的资源 35 | shrinkResources true 36 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 37 | } 38 | } 39 | compileOptions { 40 | sourceCompatibility JavaVersion.VERSION_17 41 | targetCompatibility JavaVersion.VERSION_17 42 | } 43 | kotlinOptions { 44 | jvmTarget = '17' 45 | } 46 | buildFeatures { 47 | compose true 48 | buildConfig true 49 | } 50 | composeOptions { 51 | kotlinCompilerExtensionVersion '1.5.8' 52 | } 53 | packagingOptions { 54 | resources { 55 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 56 | } 57 | dex { 58 | // 当 minSdk 为 28 或更高级别时,开启后 AGP 会在 APK 中打包压缩的 DEX 文件 59 | useLegacyPackaging true 60 | } 61 | } 62 | sourceSets { 63 | main { 64 | res { 65 | srcDirs 'src/main/res', 'src/main/res/drawble-night', 'src/main/res/drawable-night' 66 | } 67 | } 68 | } 69 | buildToolsVersion '36.0.0' 70 | ndkVersion '27.0.12077973' 71 | 72 | android.applicationVariants.configureEach { variant -> 73 | variant.outputs.each { output -> 74 | output.outputFileName = "Msea_" + variant.versionName + ".apk" 75 | } 76 | } 77 | } 78 | 79 | dependencies { 80 | // jetpack 81 | implementation 'androidx.core:core-ktx:1.15.0' 82 | implementation 'androidx.core:core-splashscreen:1.0.1' 83 | implementation 'androidx.datastore:datastore-preferences:1.1.4' 84 | 85 | // compose 86 | implementation "androidx.compose.ui:ui:$compose_version" 87 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 88 | implementation 'androidx.compose.material3:material3:1.3.1' 89 | 90 | implementation 'androidx.activity:activity-compose:1.10.1' 91 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7' 92 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7' 93 | implementation 'androidx.navigation:navigation-compose:2.8.9' 94 | implementation 'androidx.paging:paging-runtime-ktx:3.3.6' 95 | implementation 'androidx.paging:paging-compose:3.3.6' 96 | 97 | implementation "com.google.accompanist:accompanist-pager:$accompanist_version" 98 | implementation "com.google.accompanist:accompanist-swiperefresh:$accompanist_version" 99 | implementation "com.google.accompanist:accompanist-webview:$accompanist_version" 100 | implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" 101 | implementation 'com.google.code.gson:gson:2.11.0' 102 | 103 | // vendor 104 | implementation 'org.jsoup:jsoup:1.17.2' 105 | implementation 'com.squareup.okhttp3:okhttp:4.12.0' 106 | implementation 'io.coil-kt:coil-compose:2.5.0' 107 | 108 | // SDK 109 | implementation 'com.umeng.umsdk:common:9.6.7' // 友盟基础组件库(所有友盟业务SDK都依赖基础组件库) 110 | implementation 'com.umeng.umsdk:asms:1.8.6' // 必选 111 | implementation 'com.umeng.umsdk:apm:1.9.5' // U-APM产品包依赖(必选) 112 | implementation 'com.umeng.umsdk:abtest:1.0.1' // 使用U-App中ABTest能力,可选 113 | 114 | // debug 115 | testImplementation 'junit:junit:4.13.2' 116 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 117 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' 118 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 119 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 120 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" 121 | } -------------------------------------------------------------------------------- /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 22 | 23 | # 友盟 SDK 混淆 24 | -keep class com.umeng.** {*;} 25 | -keep class org.repackage.** {*;} 26 | -keepclassmembers class * { 27 | public (org.json.JSONObject); 28 | } 29 | -keepclassmembers enum * { 30 | public static **[] values(); 31 | public static ** valueOf(java.lang.String); 32 | } 33 | 34 | ##---------------Begin: proguard configuration for Gson ---------- 35 | # Gson uses generic type information stored in a class file when working with fields. Proguard 36 | # removes such information by default, so configure it to keep all of it. 37 | -keepattributes Signature 38 | 39 | # For using GSON @Expose annotation 40 | -keepattributes *Annotation* 41 | 42 | # Gson specific classes 43 | -dontwarn sun.misc.** 44 | #-keep class com.google.gson.stream.** { *; } 45 | 46 | # Application classes that will be serialized/deserialized over Gson 47 | -keep class com.google.gson.examples.android.model.** { ; } 48 | 49 | # 配置成自己的包路径 model 50 | -keep class com.eternaljust.msea.ui.data.** { *; } 51 | 52 | # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, 53 | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) 54 | -keep class * extends com.google.gson.TypeAdapter 55 | -keep class * implements com.google.gson.TypeAdapterFactory 56 | -keep class * implements com.google.gson.JsonSerializer 57 | -keep class * implements com.google.gson.JsonDeserializer 58 | 59 | # Prevent R8 from leaving Data object members always null 60 | -keepclassmembers,allowobfuscation class * { 61 | @com.google.gson.annotations.SerializedName ; 62 | } 63 | 64 | # Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. 65 | -keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken 66 | -keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken 67 | 68 | ##---------------End: proguard configuration for Gson ---------- -------------------------------------------------------------------------------- /app/src/androidTest/java/com/eternaljust/msea/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea 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.eternaljust.msea", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eternaljust/msea-compose/ccd1c7c1f6b77cbd7a77aaf0812962ef2ec8812e/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/MseaApp.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.eternaljust.msea.utils.Constants 6 | import com.eternaljust.msea.utils.DataStoreUtil 7 | import com.eternaljust.msea.utils.SettingInfo 8 | import com.umeng.cconfig.RemoteConfigSettings 9 | import com.umeng.cconfig.UMRemoteConfig 10 | import com.umeng.commonsdk.UMConfigure 11 | 12 | class MseaApp: Application() { 13 | companion object { 14 | lateinit var CONTEXT: Context 15 | } 16 | 17 | override fun onCreate() { 18 | super.onCreate() 19 | 20 | CONTEXT = this 21 | DataStoreUtil.init(this) 22 | 23 | val appkey = Constants.umAppkey 24 | val channel = "GitHub" 25 | // 友盟预初始化 26 | UMConfigure.preInit(this, appkey, channel) 27 | // 支持在子进程中统计自定义事件 28 | UMConfigure.setProcessEvent(true) 29 | if (SettingInfo.instance.agreePrivacyPolicy) { 30 | // 云配置自动更新代码逻辑 31 | UMRemoteConfig.getInstance().setConfigSettings( 32 | RemoteConfigSettings.Builder().setAutoUpdateModeEnabled(true).build() 33 | ) 34 | UMRemoteConfig.getInstance().setDefaults(R.xml.cloud_config_parms) 35 | // 友盟正式初始化 36 | UMConfigure.init(this, appkey, channel, UMConfigure.DEVICE_TYPE_PHONE, "") 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/data/JSONModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.data 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | data class WebViewModel( 8 | var url: String = "", 9 | var title: String = "", 10 | var tid: String = "" 11 | ) : Parcelable 12 | 13 | @Parcelize 14 | data class TopicDetailRouteModel( 15 | var tid: String = "", 16 | var isNodeFid125: Boolean = false 17 | ) : Parcelable 18 | 19 | @Parcelize 20 | data class ConfigVersionModel( 21 | var versionCode: Int = 0, 22 | var versionName: String = "", 23 | var versionContent: String = "", 24 | var updateTime: String = "" 25 | ) : Parcelable 26 | 27 | @Parcelize 28 | data class TagItemModel( 29 | var tid: String = "", 30 | var title: String = "" 31 | ) : Parcelable 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/home/HomePage.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.home 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.* 6 | import androidx.compose.material3.SnackbarHostState 7 | import androidx.compose.material3.Tab 8 | import androidx.compose.material3.TabRow 9 | import androidx.compose.runtime.* 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.platform.LocalContext 12 | import androidx.compose.ui.unit.dp 13 | import androidx.lifecycle.viewmodel.compose.viewModel 14 | import androidx.navigation.NavHostController 15 | import com.eternaljust.msea.R 16 | import com.eternaljust.msea.ui.page.home.topic.TopicListPage 17 | import com.eternaljust.msea.ui.page.home.topic.TopicListViewModel 18 | import com.eternaljust.msea.utils.StatisticsTool 19 | import com.google.accompanist.pager.HorizontalPager 20 | import com.google.accompanist.pager.rememberPagerState 21 | import com.google.accompanist.pager.ExperimentalPagerApi 22 | import kotlinx.coroutines.launch 23 | 24 | @OptIn(ExperimentalPagerApi::class) 25 | @Composable 26 | fun HomePage( 27 | scaffoldState: SnackbarHostState, 28 | navController: NavHostController, 29 | viewModel: HomeViewModel = viewModel() 30 | ) { 31 | val pagerState = rememberPagerState() 32 | val scope = rememberCoroutineScope() 33 | val items = viewModel.topicItems 34 | val context = LocalContext.current 35 | 36 | Surface( 37 | modifier = Modifier 38 | .padding(horizontal = 16.dp) 39 | ) { 40 | Column { 41 | TabRow(selectedTabIndex = pagerState.currentPage) { 42 | items.forEachIndexed { index, item -> 43 | Tab( 44 | text = { Text( text = item.title ) }, 45 | selected = pagerState.currentPage == index, 46 | onClick = { 47 | scope.launch { 48 | pagerState.scrollToPage(index) 49 | } 50 | StatisticsTool.instance.eventObject( 51 | context = context, 52 | resId = R.string.event_page_tab, 53 | keyAndValue = mapOf( 54 | R.string.key_name_home to item.title 55 | ) 56 | ) 57 | } 58 | ) 59 | } 60 | } 61 | 62 | HorizontalPager(count = items.size, state = pagerState) { 63 | if (it == pagerState.currentPage) { 64 | 65 | val vm = when (items[pagerState.currentPage]) { 66 | TopicTabItem.NEW -> TopicListViewModel.new 67 | TopicTabItem.HOT -> TopicListViewModel.hot 68 | TopicTabItem.NEWTHREAD -> TopicListViewModel.newthread 69 | } 70 | TopicListPage( 71 | scaffoldState = scaffoldState, 72 | navController = navController, 73 | viewModel = vm 74 | ) 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.home 2 | 3 | import androidx.lifecycle.ViewModel 4 | 5 | class HomeViewModel : ViewModel() { 6 | 7 | val topicItems: List 8 | get() = listOf( 9 | TopicTabItem.NEW, 10 | TopicTabItem.HOT, 11 | TopicTabItem.NEWTHREAD 12 | ) 13 | } 14 | 15 | interface TopicTab { 16 | val id: String 17 | val title: String 18 | } 19 | 20 | enum class TopicTabItem : TopicTab { 21 | NEW{ 22 | override val id: String 23 | get() = "new" 24 | 25 | override val title: String 26 | get() = "最新回复" 27 | }, 28 | 29 | HOT{ 30 | override val id: String 31 | get() = "hot" 32 | 33 | override val title: String 34 | get() = "最新热门" 35 | }, 36 | 37 | NEWTHREAD{ 38 | override val id: String 39 | get() = "newthread" 40 | 41 | override val title: String 42 | get() = "最新发表" 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/home/search/SearchPostViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.home.search 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.eternaljust.msea.utils.HTMLURL 9 | import com.eternaljust.msea.utils.NetworkUtil 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.launch 12 | 13 | class SearchPostViewModel: ViewModel() { 14 | private var keyword = "" 15 | private var page = 1 16 | private var pageLoadCompleted = true 17 | private var href = "" 18 | 19 | var viewStates by mutableStateOf(SearchPostState()) 20 | private set 21 | val pageSize: Int 22 | get() = 18 23 | 24 | fun dispatch(action: SearchPostAction) { 25 | when (action) { 26 | is SearchPostAction.SearchKeyword -> { 27 | if (keyword != action.content) { 28 | keyword = action.content 29 | if (action.content.isNotEmpty()) { 30 | page = 1 31 | href = "" 32 | loadMoreData() 33 | } 34 | } 35 | } 36 | is SearchPostAction.LoadMoreData -> { 37 | if (keyword.isNotEmpty()) { 38 | page += 1 39 | loadMoreData() 40 | } 41 | } 42 | } 43 | } 44 | 45 | private fun loadMoreData() { 46 | println("---开始搜索帖子:$keyword") 47 | pageLoadCompleted = false 48 | if (page == 1) { 49 | viewStates = viewStates.copy(list = emptyList(), isRefreshing = true) 50 | } 51 | 52 | var list = mutableListOf() 53 | 54 | viewModelScope.launch(Dispatchers.IO) { 55 | val srchtxt = NetworkUtil.urlEncode(keyword) 56 | var url = HTMLURL.SEARCH_POST + "&srchtxt=${srchtxt}" 57 | if (page > 1 && href.isNotEmpty()) { 58 | url = "${HTMLURL.BASE}/$href" 59 | } 60 | val document = NetworkUtil.getRequest(url) 61 | val a = document.selectXpath("//div[@class='pgs cl mbm']/div/a") 62 | a.forEach { 63 | val text = it.text() 64 | if (text == (page + 1).toString()) { 65 | val aHref = it.attr("href") 66 | if (aHref.isNotEmpty()) { 67 | href = aHref 68 | } 69 | } 70 | } 71 | 72 | val li = document.selectXpath("//ul/li[@class='pbw']") 73 | li.forEach { 74 | val search = SearchPostListModel() 75 | search.keyword = keyword 76 | val tid = it.attr("id") 77 | if (tid.isNotEmpty()) { 78 | search.tid = tid 79 | } 80 | val title = it.selectXpath("h3/a[1]").text() 81 | if (title.isNotEmpty()) { 82 | search.title = title 83 | } 84 | val views = it.selectXpath("p[@class='xg1']").text() 85 | if (views.isNotEmpty()) { 86 | search.replyViews = views 87 | } 88 | val content = it.selectXpath("p[2]").text() 89 | if (content.isNotEmpty()) { 90 | search.content = content 91 | } 92 | val time = it.selectXpath("p[3]/span[1]").text() 93 | if (time.isNotEmpty()) { 94 | search.time = time 95 | } 96 | val name = it.selectXpath("p[3]/span[2]/a").text() 97 | if (name.isNotEmpty()) { 98 | search.name = name 99 | } 100 | val plate = it.selectXpath("p[3]/span[3]/a").text() 101 | if (plate.isNotEmpty()) { 102 | search.plate = plate 103 | } 104 | val fid = it.selectXpath("p[3]/span[3]/a").attr("href") 105 | if (fid.isNotEmpty()) { 106 | search.fid = NetworkUtil.getFid(fid) 107 | } 108 | list.add(search) 109 | 110 | if (list.count() == 9) { 111 | if (page == 1 && viewStates.isRefreshing) { 112 | viewStates = viewStates.copy(list = emptyList()) 113 | } 114 | viewStates = viewStates.copy( 115 | list = viewStates.list + list, 116 | isRefreshing = false 117 | ) 118 | list = mutableListOf() 119 | } 120 | // 列表最后一个 121 | li.last()?.let { last -> 122 | if (last == it) { 123 | pageLoadCompleted = true 124 | if (li.count() != pageSize) { 125 | viewStates = viewStates.copy( 126 | list = viewStates.list + list, 127 | isRefreshing = false 128 | ) 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | 137 | data class SearchPostState( 138 | val list: List = emptyList(), 139 | val isRefreshing: Boolean = false 140 | ) 141 | 142 | sealed class SearchPostAction { 143 | object LoadMoreData: SearchPostAction() 144 | 145 | data class SearchKeyword(val content: String) : SearchPostAction() 146 | } 147 | 148 | class SearchPostListModel { 149 | var fid = "" 150 | var tid = "" 151 | var title = "" 152 | var content = "" 153 | var time = "" 154 | var replyViews = "" 155 | var name = "" 156 | var plate = "" 157 | var keyword = "" 158 | } 159 | -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/home/search/SearchUserPage.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.home.search 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.lazy.items 8 | import androidx.compose.foundation.lazy.itemsIndexed 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.* 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.DisposableEffect 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.res.painterResource 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.unit.dp 20 | import androidx.lifecycle.viewmodel.compose.viewModel 21 | import androidx.navigation.NavHostController 22 | import coil.compose.AsyncImage 23 | import com.eternaljust.msea.R 24 | import com.eternaljust.msea.ui.widget.ListArrowForward 25 | import com.eternaljust.msea.utils.RouteName 26 | import com.eternaljust.msea.utils.StatisticsTool 27 | import com.eternaljust.msea.utils.UserInfo 28 | 29 | @OptIn(ExperimentalFoundationApi::class) 30 | @Composable 31 | fun SearchUserPage( 32 | scaffoldState: SnackbarHostState, 33 | navController: NavHostController, 34 | keyword: String, 35 | viewModel: SearchUserViewModel = viewModel() 36 | ) { 37 | val context = LocalContext.current 38 | 39 | if (UserInfo.instance.auth.isNotEmpty()) { 40 | viewModel.dispatch(SearchUserAction.SearchKeyword(keyword)) 41 | } 42 | 43 | Column( 44 | modifier = Modifier 45 | .fillMaxSize(), 46 | horizontalAlignment = Alignment.CenterHorizontally, 47 | verticalArrangement = Arrangement.Center 48 | ) { 49 | if (viewModel.viewStates.list.isEmpty()) { 50 | if (keyword.isNotEmpty()) { 51 | Text("没有找到\"${keyword}\"相关用户") 52 | } 53 | } else { 54 | LazyColumn( 55 | modifier = Modifier.fillMaxSize(), 56 | horizontalAlignment = Alignment.CenterHorizontally, 57 | ) { 58 | stickyHeader { 59 | SearchListHeader( 60 | count = "以下是查找到的用户列表(${viewModel.viewStates.list.count()})个" 61 | ) 62 | } 63 | 64 | items(viewModel.viewStates.list) { 65 | SearchUserListItemContent( 66 | item = it, 67 | contentClick = { 68 | navController.navigate(RouteName.PROFILE_DETAIL + "/${it.uid}") 69 | StatisticsTool.instance.eventObject( 70 | context = context, 71 | resId = R.string.event_page_profile, 72 | keyAndValue = mapOf( 73 | R.string.key_source to "搜索用户" 74 | ) 75 | ) 76 | } 77 | ) 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | @Composable 85 | fun SearchListHeader( 86 | count: String 87 | ) { 88 | Card( 89 | modifier = Modifier 90 | .fillMaxWidth() 91 | .padding(vertical = 8.dp) 92 | ) { 93 | Row( 94 | modifier = Modifier 95 | .fillMaxWidth() 96 | .padding(5.dp), 97 | verticalAlignment = Alignment.CenterVertically 98 | ) { 99 | Text(text = count) 100 | } 101 | } 102 | } 103 | 104 | @Composable 105 | fun SearchUserListItemContent( 106 | item: UserListModel, 107 | contentClick: () -> Unit 108 | ) { 109 | Row( 110 | modifier = Modifier 111 | .fillMaxWidth() 112 | .padding(vertical = 10.dp) 113 | .clickable { contentClick() } 114 | ) { 115 | AsyncImage( 116 | modifier = Modifier 117 | .size(45.dp) 118 | .clip(shape = RoundedCornerShape(5)), 119 | model = item.avatar, 120 | placeholder = painterResource(id = R.drawable.icon), 121 | contentDescription = null, 122 | ) 123 | 124 | Spacer(modifier = Modifier.width(10.dp)) 125 | 126 | Row( 127 | modifier = Modifier 128 | .fillMaxWidth(), 129 | verticalAlignment = Alignment.CenterVertically, 130 | horizontalArrangement = Arrangement.SpaceBetween 131 | ) { 132 | Column { 133 | Text( 134 | text = item.name, 135 | style = MaterialTheme.typography.titleMedium, 136 | fontWeight = FontWeight.Bold 137 | ) 138 | 139 | Spacer(modifier = Modifier.height(5.dp)) 140 | 141 | Text( 142 | text = item.content 143 | ) 144 | } 145 | 146 | ListArrowForward() 147 | } 148 | } 149 | 150 | Divider(modifier = Modifier) 151 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/home/search/SearchUserViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.home.search 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.eternaljust.msea.utils.HTMLURL 9 | import com.eternaljust.msea.utils.NetworkUtil 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.launch 12 | 13 | class SearchUserViewModel: ViewModel() { 14 | var viewStates by mutableStateOf(SearchUserViewState()) 15 | private set 16 | val pageSize: Int 17 | get() = 100 18 | 19 | private var keyword = "" 20 | private var pageLoadCompleted = true 21 | 22 | fun dispatch(action: SearchUserAction) { 23 | when (action) { 24 | is SearchUserAction.SearchKeyword -> { 25 | if (keyword != action.content) { 26 | keyword = action.content 27 | if (action.content.isNotEmpty()) { 28 | loadMoreData() 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | private fun loadMoreData() { 36 | println("---开始搜索用户:$keyword") 37 | pageLoadCompleted = false 38 | viewStates = viewStates.copy(isRefreshing = true) 39 | 40 | var list = mutableListOf() 41 | viewModelScope.launch(Dispatchers.IO) { 42 | val url = HTMLURL.SEARCH_USER + "&username=$keyword" 43 | val document = NetworkUtil.getRequest(url) 44 | val lis = document.selectXpath("//li[@class='bbda cl']") 45 | lis.forEach { 46 | var user = UserListModel() 47 | val avatar = it.selectXpath("div[@class='avt']/a/img").attr("src") 48 | if (avatar.isNotEmpty()) { 49 | user.avatar = NetworkUtil.getAvatar(avatar) 50 | } 51 | val name = it.selectXpath("h4/a").attr("title") 52 | if (name.isNotEmpty()) { 53 | user.name = name 54 | } 55 | val content = it.selectXpath("p[@class='maxh']").text() 56 | if (content.isNotEmpty()) { 57 | user.content = content.replace("\r\n", "") 58 | } 59 | val uid = it.selectXpath("h4/a").attr("href") 60 | if (uid.isNotEmpty()) { 61 | user.uid = NetworkUtil.getUid(uid) 62 | } 63 | println("user=${user.avatar}-${user.name}-${user.content}-${user.uid}") 64 | list.add(user) 65 | 66 | if (list.count() == 10) { 67 | if (viewStates.isRefreshing) { 68 | viewStates = viewStates.copy(list = emptyList()) 69 | } 70 | viewStates = viewStates.copy( 71 | list = viewStates.list + list, 72 | isRefreshing = false 73 | ) 74 | list = mutableListOf() 75 | } 76 | // 列表最后一个 77 | lis.last()?.let { last -> 78 | if (last == it) { 79 | pageLoadCompleted = true 80 | if (lis.count() != pageSize) { 81 | viewStates = viewStates.copy( 82 | list = viewStates.list + list, 83 | isRefreshing = false 84 | ) 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | data class SearchUserViewState( 94 | val list: List = emptyList(), 95 | val isRefreshing: Boolean = false 96 | ) 97 | sealed class SearchUserAction { 98 | data class SearchKeyword(val content: String) : SearchUserAction() 99 | } 100 | 101 | class UserListModel { 102 | var uid = "" 103 | var avatar = "" 104 | var content = "" 105 | var name = "" 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/home/search/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.home.search 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import kotlinx.coroutines.channels.Channel 9 | import kotlinx.coroutines.flow.receiveAsFlow 10 | import kotlinx.coroutines.launch 11 | 12 | class SearchViewModel : ViewModel() { 13 | val items: List 14 | get() = listOf( 15 | SearchTabItem.POST, 16 | SearchTabItem.USER 17 | ) 18 | 19 | var viewStates by mutableStateOf(SearchViewStates()) 20 | private set 21 | private val _viewEvents = Channel(Channel.BUFFERED) 22 | val viewEvents = _viewEvents.receiveAsFlow() 23 | 24 | fun dispatch(action: SearchViewAction) { 25 | when (action) { 26 | is SearchViewAction.PopBack -> { 27 | viewModelScope.launch { 28 | _viewEvents.send(SearchViewEvent.PopBack) 29 | } 30 | } 31 | is SearchViewAction.SearchKeyboard -> { 32 | viewModelScope.launch { 33 | _viewEvents.send(SearchViewEvent.SearchKeyboard) 34 | } 35 | viewStates = viewStates.copy(searchContent = viewStates.keyword) 36 | } 37 | is SearchViewAction.UpdateKeyword -> { 38 | viewStates = viewStates.copy(keyword = action.content) 39 | } 40 | } 41 | } 42 | } 43 | 44 | data class SearchViewStates( 45 | val keyword: String = "", 46 | val searchContent: String = "" 47 | ) 48 | 49 | sealed class SearchViewEvent { 50 | object PopBack: SearchViewEvent() 51 | object SearchKeyboard: SearchViewEvent() 52 | } 53 | 54 | sealed class SearchViewAction { 55 | object PopBack: SearchViewAction() 56 | object SearchKeyboard: SearchViewAction() 57 | 58 | data class UpdateKeyword(val content: String) : SearchViewAction() 59 | } 60 | 61 | interface SearchTab { 62 | val id: String 63 | val title: String 64 | } 65 | 66 | enum class SearchTabItem : SearchTab { 67 | POST { 68 | override val id: String 69 | get() = "post" 70 | 71 | override val title: String 72 | get() = "帖子" 73 | }, 74 | 75 | USER { 76 | override val id: String 77 | get() = "user" 78 | 79 | override val title: String 80 | get() = "用户" 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/home/sign/SignListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.home.sign 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.paging.* 8 | import com.eternaljust.msea.utils.HTMLURL 9 | import com.eternaljust.msea.utils.NetworkUtil 10 | import com.eternaljust.msea.utils.configPager 11 | import kotlinx.coroutines.* 12 | import kotlinx.coroutines.flow.Flow 13 | import java.util.UUID 14 | 15 | /** 16 | * 今日签到列表 17 | */ 18 | class SignListViewModel : ViewModel() { 19 | private val pager by lazy { 20 | configPager(PagingConfig(pageSize = 7, prefetchDistance = 1)) { 21 | loadData(page = it) 22 | } 23 | } 24 | 25 | var viewStates by mutableStateOf(SignListViewState(pagingData = pager)) 26 | private set 27 | 28 | private suspend fun loadData(page: Int) : List { 29 | val list = mutableListOf() 30 | 31 | withContext(Dispatchers.IO) { 32 | val url = HTMLURL.SIGN_LIST + "&ac=daysign&page=${page}" 33 | val document = NetworkUtil.getRequest(url) 34 | val trs = document.selectXpath("//div[@class='wqpc_sign_table']/div/table//tr") 35 | trs.forEach { 36 | val signModel = SignListModel() 37 | val no = it.selectXpath("td[1]").text() 38 | if (no.isNotEmpty() && no.contains("NO.")) { 39 | signModel.no = no.replace("NO.", "") 40 | 41 | val name = it.selectXpath("td[2]//a").text() 42 | if (name.isNotEmpty()) { 43 | signModel.name = name 44 | } 45 | 46 | val href = it.selectXpath("td[2]//a").attr("href") 47 | if (href.contains("uid-")) { 48 | signModel.uid = NetworkUtil.getUid(href) 49 | } 50 | 51 | val content = it.selectXpath("td[3]/p").text() 52 | if (content.isNotEmpty()) { 53 | signModel.content = content 54 | } 55 | 56 | val bits = it.selectXpath("td[4]/span").text() 57 | if (bits.isNotEmpty()) { 58 | signModel.bits = bits 59 | } 60 | 61 | val time = it.selectXpath("td[5]/span").text() 62 | if (time.isNotEmpty()) { 63 | signModel.time = time 64 | } 65 | 66 | list.add(signModel) 67 | } 68 | } 69 | } 70 | 71 | return list 72 | } 73 | } 74 | 75 | data class SignListViewState( 76 | val pagingData: Flow> 77 | ) 78 | 79 | class SignListModel { 80 | val uuid = UUID.randomUUID() 81 | var uid = "" 82 | var no = "" 83 | var name = "" 84 | var content = "" 85 | var time = "" 86 | var bits = "" 87 | } 88 | 89 | /** 90 | * 总天数、总奖励排行 91 | */ 92 | class SignDayListViewModel( 93 | val tabItem: SignTabItem 94 | ) : ViewModel() { 95 | companion object { 96 | val days by lazy { SignDayListViewModel(tabItem = SignTabItem.TOTAL_DAYS) } 97 | val reward by lazy { SignDayListViewModel(tabItem = SignTabItem.TOTAL_REWARD) } 98 | } 99 | 100 | private val pager by lazy { 101 | configPager(PagingConfig(pageSize = 7, initialLoadSize = 1)) { 102 | loadData(page = it) 103 | } 104 | } 105 | 106 | var viewStates by mutableStateOf(SignDayListViewState(pagingData = pager)) 107 | private set 108 | 109 | private suspend fun loadData(page: Int) : List { 110 | val list = mutableListOf() 111 | 112 | withContext(Dispatchers.IO) { 113 | val url = HTMLURL.SIGN_LIST + "&ac=daysign&ac=${tabItem.id}&page=${page}" 114 | val document = NetworkUtil.getRequest(url) 115 | val trs = document.selectXpath("//div[@class='wqpc_sign_table']/div/table//tr") 116 | trs.forEach { 117 | val signModel = SignDayListModel() 118 | val no = it.selectXpath("td[1]").text() 119 | if (no.isNotEmpty() && no.contains("NO.")) { 120 | signModel.no = no.replace("NO.", "") 121 | 122 | val name = it.selectXpath("td[2]//a").text() 123 | if (name.isNotEmpty()) { 124 | signModel.name = name 125 | } 126 | 127 | val href = it.selectXpath("td[2]//a").attr("href") 128 | if (href.contains("uid-")) { 129 | signModel.uid = NetworkUtil.getUid(href) 130 | } 131 | 132 | val continuous = it.selectXpath("td[3]").text() 133 | if (continuous.isNotEmpty()) { 134 | signModel.continuous = continuous 135 | } 136 | 137 | val month = it.selectXpath("td[4]").text() 138 | if (month.isNotEmpty()) { 139 | signModel.month = month 140 | } 141 | 142 | val total = it.selectXpath("td[5]").text() 143 | if (total.isNotEmpty()) { 144 | signModel.total = total 145 | } 146 | 147 | val bits = it.selectXpath("td[6]/span").text() 148 | if (bits.isNotEmpty()) { 149 | signModel.bits = bits 150 | } 151 | 152 | val time = it.selectXpath("td[7]").text() 153 | if (time.isNotEmpty()) { 154 | signModel.time = time.replace(" ", "\n") 155 | } 156 | 157 | list.add(signModel) 158 | } 159 | } 160 | } 161 | 162 | return list 163 | } 164 | } 165 | 166 | data class SignDayListViewState( 167 | val pagingData: Flow> 168 | ) 169 | 170 | class SignDayListModel { 171 | val uuid = UUID.randomUUID() 172 | var uid = "" 173 | var no = "" 174 | var name = "" 175 | var time = "" 176 | var bits = "" 177 | var continuous = "" 178 | var month = "" 179 | var total = "" 180 | } 181 | -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/node/tag/TagListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.node.tag 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import androidx.paging.PagingConfig 9 | import androidx.paging.PagingData 10 | import com.eternaljust.msea.utils.HTMLURL 11 | import com.eternaljust.msea.utils.NetworkUtil 12 | import com.eternaljust.msea.utils.configPager 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.channels.Channel 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.receiveAsFlow 17 | import kotlinx.coroutines.launch 18 | import kotlinx.coroutines.withContext 19 | import java.util.UUID 20 | 21 | class TagListViewModel : ViewModel() { 22 | private var tid: String = "" 23 | 24 | private val pager by lazy { 25 | configPager(PagingConfig(pageSize = 30, prefetchDistance = 1)) { 26 | loadData(tid = tid, page = it) 27 | } 28 | } 29 | 30 | var viewStates by mutableStateOf(TagListViewState(pagingData = pager)) 31 | private set 32 | 33 | private val _viewEvents = Channel(Channel.BUFFERED) 34 | val viewEvents = _viewEvents.receiveAsFlow() 35 | 36 | fun dispatch(action: TagListViewAction) { 37 | when (action) { 38 | is TagListViewAction.PopBack -> popBack() 39 | is TagListViewAction.SetTid -> tid = action.id 40 | } 41 | } 42 | 43 | private suspend fun loadData(tid: String, page: Int) : List { 44 | val list = mutableListOf() 45 | if (page > 1) { 46 | return list 47 | } 48 | 49 | withContext(Dispatchers.IO) { 50 | val url = HTMLURL.TAG_LIST + "&id=$tid" 51 | val document = NetworkUtil.getRequest(url) 52 | val tr = document.selectXpath("//div[@class='bm_c']/table/tbody/tr") 53 | tr.forEach { 54 | val tag = TagListModel() 55 | 56 | val gif = it.selectXpath("td[@class='icn']/a/img").attr("src") 57 | if (gif.isNotEmpty()) { 58 | tag.gif = HTMLURL.BASE + "/${gif}" 59 | } 60 | val title = it.selectXpath("th/a").text() 61 | if (title.isNotEmpty()) { 62 | tag.title = title 63 | } 64 | val id = it.selectXpath("th/a").attr("href") 65 | if (id.isNotEmpty()) { 66 | tag.tid = id.split("thread-").last().split("-").first() 67 | } 68 | val forum = it.selectXpath("td[@class='by']/a").text() 69 | if (forum.isNotEmpty()) { 70 | tag.forum = forum 71 | } 72 | val fid = it.selectXpath("td[@class='by']/a").attr("href") 73 | println("fid---$fid") 74 | if (fid.isNotEmpty()) { 75 | tag.fid = NetworkUtil.getFid(fid) 76 | } 77 | val name = it.selectXpath("td[@class='by'][2]/cite/a").text() 78 | if (name.isNotEmpty()) { 79 | tag.name = name 80 | } 81 | val uid = it.selectXpath("td[@class='by'][2]/cite/a").attr("href") 82 | if (uid.isNotEmpty()) { 83 | tag.uid = uid 84 | } 85 | val time = it.selectXpath("td[@class='by']/em/span").text() 86 | if (time.isNotEmpty()) { 87 | tag.time = time 88 | } 89 | val reply = it.selectXpath("td[@class='num']/a").text() 90 | if (reply.isNotEmpty()) { 91 | tag.reply = reply 92 | } 93 | val examine = it.selectXpath("td[@class='num']/em").text() 94 | if (examine.isNotEmpty()) { 95 | tag.examine = examine 96 | } 97 | val lastName = it.selectXpath("td[@class='by'][last()]/cite/a").text() 98 | if (lastName.isNotEmpty()) { 99 | tag.lastName = lastName 100 | tag.lastUserName = "space-username-${lastName}.html" 101 | } 102 | val lastTime = it.selectXpath("td[@class='by'][last()]/em/a").text() 103 | if (lastTime.isNotEmpty()) { 104 | tag.lastTime = lastTime 105 | } 106 | 107 | list.add(tag) 108 | } 109 | } 110 | 111 | return list 112 | } 113 | 114 | private fun popBack() { 115 | viewModelScope.launch { 116 | _viewEvents.send(TagListViewEvent.PopBack) 117 | } 118 | } 119 | } 120 | 121 | data class TagListViewState( 122 | val pagingData: Flow> 123 | ) 124 | 125 | sealed class TagListViewEvent { 126 | object PopBack : TagListViewEvent() 127 | } 128 | 129 | sealed class TagListViewAction { 130 | object PopBack: TagListViewAction() 131 | 132 | data class SetTid(val id: String) : TagListViewAction() 133 | } 134 | 135 | class TagListModel { 136 | val uuid = UUID.randomUUID() 137 | /** 138 | * 帖子标题 139 | */ 140 | var title = "" 141 | /** 142 | * 帖子链接 id 143 | */ 144 | var tid = "" 145 | /** 146 | * 帖子动图 147 | */ 148 | var gif = "" 149 | /** 150 | * 帖子板块 151 | */ 152 | var forum = "" 153 | /** 154 | * 帖子板块 id 155 | */ 156 | var fid = "" 157 | /** 158 | * 帖子作者 159 | */ 160 | var name = "" 161 | /** 162 | * 帖子作者 id 163 | */ 164 | var uid = "" 165 | /** 166 | * 帖子发表时间 167 | */ 168 | var time = "" 169 | /** 170 | * 查看 171 | */ 172 | var examine = "" 173 | /** 174 | * 回复 175 | */ 176 | var reply = "" 177 | /** 178 | * 最后发表的昵称 179 | */ 180 | var lastName = "" 181 | /** 182 | * 最后发表的用户 183 | */ 184 | var lastUserName = "" 185 | /** 186 | * 最后发表的时间 187 | */ 188 | var lastTime = "" 189 | } 190 | -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/node/tag/TagPage.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.node.tag 2 | 3 | import android.net.Uri 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.lazy.grid.GridCells 8 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 9 | import androidx.compose.foundation.lazy.grid.items 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material3.* 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.LaunchedEffect 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.unit.dp 19 | import androidx.lifecycle.viewmodel.compose.viewModel 20 | import androidx.navigation.NavHostController 21 | import com.eternaljust.msea.ui.widget.NormalTopAppBar 22 | import com.eternaljust.msea.utils.RouteName 23 | import com.eternaljust.msea.utils.toJson 24 | 25 | @OptIn(ExperimentalMaterial3Api::class) 26 | @Composable 27 | fun TagPage( 28 | scaffoldState: SnackbarHostState, 29 | navController: NavHostController, 30 | viewModel: TagViewModel = viewModel() 31 | ) { 32 | viewModel.dispatch(TagViewAction.GetTagList) 33 | LaunchedEffect(Unit) { 34 | viewModel.viewEvents.collect { 35 | when (it) { 36 | is TagViewEvent.PopBack -> { 37 | navController.popBackStack() 38 | } 39 | } 40 | } 41 | } 42 | 43 | Scaffold( 44 | topBar = { 45 | NormalTopAppBar( 46 | title = "标签", 47 | onClick = { viewModel.dispatch(TagViewAction.PopBack) } 48 | ) 49 | }, 50 | content = { paddingValues -> 51 | Surface( 52 | modifier = Modifier 53 | .padding(paddingValues) 54 | ) { 55 | LazyVerticalGrid( 56 | contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), 57 | verticalArrangement = Arrangement.spacedBy(16.dp), 58 | horizontalArrangement = Arrangement.spacedBy(16.dp), 59 | columns = GridCells.Fixed(count = 3), 60 | content = { 61 | items(viewModel.viewStates.list) { 62 | val args = String.format("/%s", Uri.encode(it.toJson())) 63 | Column( 64 | modifier = Modifier 65 | .clip(shape = RoundedCornerShape(50.dp)) 66 | .background(MaterialTheme.colorScheme.secondary) 67 | .clickable { navController.navigate(RouteName.TAG_LIST + args) }, 68 | verticalArrangement = Arrangement.Center, 69 | horizontalAlignment = Alignment.CenterHorizontally 70 | ) { 71 | Text( 72 | modifier = Modifier 73 | .padding(vertical = 8.dp), 74 | text = it.title, 75 | color = Color.White 76 | ) 77 | } 78 | } 79 | }) 80 | } 81 | } 82 | ) 83 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/node/tag/TagViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.node.tag 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.eternaljust.msea.ui.data.TagItemModel 9 | import com.eternaljust.msea.utils.HTMLURL 10 | import com.eternaljust.msea.utils.NetworkUtil 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.channels.Channel 13 | import kotlinx.coroutines.flow.receiveAsFlow 14 | import kotlinx.coroutines.launch 15 | 16 | class TagViewModel : ViewModel() { 17 | var viewStates by mutableStateOf(TagViewState()) 18 | private set 19 | private val _viewEvents = Channel(Channel.BUFFERED) 20 | val viewEvents = _viewEvents.receiveAsFlow() 21 | 22 | fun dispatch(action: TagViewAction) { 23 | when (action) { 24 | TagViewAction.GetTagList -> loadData() 25 | TagViewAction.PopBack -> popBack() 26 | } 27 | } 28 | 29 | private fun loadData() { 30 | viewModelScope.launch(Dispatchers.IO) { 31 | val list = mutableListOf() 32 | 33 | val url = HTMLURL.TAG_LIST 34 | val document = NetworkUtil.getRequest(url) 35 | val taglist = document.selectXpath("//div[@class='taglist mtm mbm']/a") 36 | 37 | taglist.forEach { 38 | val item = TagItemModel() 39 | 40 | val title = it.attr("title") 41 | if (title.isNotEmpty()) { 42 | item.title = title 43 | } 44 | val href = it.attr("href") 45 | if (href.contains("id=")) { 46 | item.tid = href.split("id=").last() 47 | } 48 | 49 | list.add(item) 50 | } 51 | 52 | viewStates = viewStates.copy(list = list) 53 | } 54 | } 55 | 56 | private fun popBack() { 57 | viewModelScope.launch { 58 | _viewEvents.send(TagViewEvent.PopBack) 59 | } 60 | } 61 | } 62 | 63 | data class TagViewState( 64 | val list: List = emptyList() 65 | ) 66 | 67 | sealed class TagViewEvent { 68 | object PopBack : TagViewEvent() 69 | } 70 | 71 | sealed class TagViewAction { 72 | object GetTagList : TagViewAction() 73 | object PopBack: TagViewAction() 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/notice/NoticePage.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.notice 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material3.* 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.rememberCoroutineScope 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.unit.dp 11 | import androidx.lifecycle.viewmodel.compose.viewModel 12 | import androidx.navigation.NavHostController 13 | import com.eternaljust.msea.R 14 | import com.eternaljust.msea.ui.page.notice.interactive.InteractivePage 15 | import com.eternaljust.msea.ui.page.notice.post.MyPostPage 16 | import com.eternaljust.msea.ui.page.notice.system.SystemPage 17 | import com.eternaljust.msea.utils.RouteName 18 | import com.eternaljust.msea.utils.StatisticsTool 19 | import com.eternaljust.msea.utils.UserInfo 20 | import com.google.accompanist.pager.ExperimentalPagerApi 21 | import com.google.accompanist.pager.HorizontalPager 22 | import com.google.accompanist.pager.rememberPagerState 23 | import kotlinx.coroutines.launch 24 | 25 | @OptIn(ExperimentalPagerApi::class) 26 | @Composable 27 | fun NoticePage( 28 | scaffoldState: SnackbarHostState, 29 | navController: NavHostController, 30 | viewModel: NoticeViewModel = viewModel() 31 | ) { 32 | val pagerState = rememberPagerState() 33 | val scope = rememberCoroutineScope() 34 | val items = viewModel.items 35 | val isLogin = UserInfo.instance.auth.isNotEmpty() 36 | val context = LocalContext.current 37 | 38 | Surface( 39 | modifier = Modifier 40 | .padding(horizontal = 16.dp) 41 | ) { 42 | Column( 43 | modifier = Modifier 44 | .fillMaxSize(), 45 | horizontalAlignment = Alignment.CenterHorizontally, 46 | verticalArrangement = Arrangement.Center 47 | ) { 48 | if (!isLogin) { 49 | Button( 50 | onClick = { 51 | navController.navigate(RouteName.LOGIN) 52 | StatisticsTool.instance.eventObject( 53 | context = context, 54 | resId = R.string.event_page_login, 55 | keyAndValue = mapOf(R.string.key_source to "通知") 56 | ) 57 | } 58 | ) { 59 | Text(text = "登录") 60 | } 61 | } else { 62 | TabRow(selectedTabIndex = pagerState.currentPage) { 63 | items.forEachIndexed { index, item -> 64 | Tab( 65 | text = { Text(item.title) }, 66 | selected = pagerState.currentPage == index, 67 | onClick = { 68 | scope.launch { 69 | pagerState.scrollToPage(index) 70 | } 71 | StatisticsTool.instance.eventObject( 72 | context = context, 73 | resId = R.string.event_page_tab, 74 | keyAndValue = mapOf( 75 | R.string.key_name_notice to item.title 76 | ) 77 | ) 78 | } 79 | ) 80 | } 81 | } 82 | 83 | HorizontalPager(count = items.size, state = pagerState) { 84 | if (it == pagerState.currentPage) { 85 | when (items[pagerState.currentPage]) { 86 | NoticeTabItem.MYPOST -> MyPostPage( 87 | scaffoldState = scaffoldState, 88 | navController = navController 89 | ) 90 | NoticeTabItem.INTERACTIVE -> InteractivePage( 91 | scaffoldState = scaffoldState, 92 | navController = navController 93 | ) 94 | NoticeTabItem.SYSTEM -> SystemPage( 95 | scaffoldState = scaffoldState, 96 | navController = navController 97 | ) 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/notice/NoticeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.notice 2 | 3 | import androidx.lifecycle.ViewModel 4 | 5 | class NoticeViewModel : ViewModel() { 6 | val items: List 7 | get() = listOf( 8 | NoticeTabItem.MYPOST, 9 | NoticeTabItem.INTERACTIVE, 10 | NoticeTabItem.SYSTEM 11 | ) 12 | } 13 | 14 | interface NoticeTab { 15 | val id: String 16 | val title: String 17 | } 18 | 19 | enum class NoticeTabItem : NoticeTab { 20 | MYPOST { 21 | override val id: String 22 | get() = "mypost" 23 | 24 | override val title: String 25 | get() = "我的帖子" 26 | }, 27 | 28 | INTERACTIVE { 29 | override val id: String 30 | get() = "interactive" 31 | 32 | override val title: String 33 | get() = "坛友互动" 34 | }, 35 | 36 | SYSTEM { 37 | override val id: String 38 | get() = "system" 39 | 40 | override val title: String 41 | get() = "系统提醒" 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/notice/interactive/InteractivePage.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.notice.interactive 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.foundation.text.ClickableText 7 | import androidx.compose.material3.Divider 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.SnackbarHostState 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.res.painterResource 18 | import androidx.compose.ui.text.SpanStyle 19 | import androidx.compose.ui.text.buildAnnotatedString 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.text.withStyle 22 | import androidx.compose.ui.unit.dp 23 | import androidx.lifecycle.viewmodel.compose.viewModel 24 | import androidx.navigation.NavHostController 25 | import androidx.paging.compose.collectAsLazyPagingItems 26 | import androidx.paging.compose.itemKey 27 | import coil.compose.AsyncImage 28 | import com.eternaljust.msea.R 29 | import com.eternaljust.msea.ui.page.notice.NoticeTabItem 30 | import com.eternaljust.msea.ui.theme.colorTheme 31 | import com.eternaljust.msea.ui.widget.RefreshList 32 | import com.eternaljust.msea.utils.RouteName 33 | import com.eternaljust.msea.utils.StatisticsTool 34 | 35 | @Composable 36 | fun InteractivePage( 37 | scaffoldState: SnackbarHostState, 38 | navController: NavHostController, 39 | viewModel: InteractiveViewModel = viewModel() 40 | ) { 41 | val viewStates = viewModel.viewStates 42 | val lazyPagingItems = viewStates.pagingData.collectAsLazyPagingItems() 43 | val context = LocalContext.current 44 | 45 | RefreshList( 46 | lazyPagingItems = lazyPagingItems 47 | ) { 48 | items( 49 | count = lazyPagingItems.itemCount, 50 | key = lazyPagingItems.itemKey { it.uuid }, 51 | ) { index -> 52 | val item = lazyPagingItems[index] 53 | item?.let { 54 | InteractiveListItemContent( 55 | item = it, 56 | avatarClick = { 57 | navController.navigate(RouteName.PROFILE_DETAIL + "/${it.uid}") 58 | StatisticsTool.instance.eventObject( 59 | context = context, 60 | resId = R.string.event_page_profile, 61 | keyAndValue = mapOf( 62 | R.string.key_source to NoticeTabItem.INTERACTIVE.title 63 | ) 64 | ) 65 | StatisticsTool.instance.eventObject( 66 | context = context, 67 | resId = R.string.event_page_notice, 68 | keyAndValue = mapOf( 69 | R.string.key_action to "个人空间" 70 | ) 71 | ) 72 | } 73 | ) 74 | } 75 | } 76 | } 77 | } 78 | 79 | @Composable 80 | fun InteractiveListItemContent( 81 | item: InteractiveFriendListModel, 82 | avatarClick: () -> Unit 83 | ) { 84 | Row( 85 | modifier = Modifier 86 | .fillMaxWidth() 87 | .padding(vertical = 10.dp), 88 | verticalAlignment = Alignment.CenterVertically 89 | ) { 90 | AsyncImage( 91 | modifier = Modifier 92 | .size(45.dp) 93 | .clip(shape = RoundedCornerShape(5)) 94 | .clickable { avatarClick() }, 95 | model = item.avatar, 96 | placeholder = painterResource(id = R.drawable.icon), 97 | contentDescription = null 98 | ) 99 | 100 | Spacer(modifier = Modifier.width(10.dp)) 101 | 102 | Column( 103 | verticalArrangement = Arrangement.Center 104 | ) { 105 | Text( 106 | text = item.time, 107 | style = MaterialTheme.typography.labelMedium, 108 | fontWeight = FontWeight.Normal 109 | ) 110 | 111 | val annotatedText = buildAnnotatedString { 112 | pushStringAnnotation( 113 | tag = "avatar", 114 | annotation = "" 115 | ) 116 | withStyle( 117 | style = SpanStyle( 118 | color = MaterialTheme.colorScheme.primary 119 | ) 120 | ) { 121 | append(item.name) 122 | } 123 | pop() 124 | 125 | withStyle( 126 | style = SpanStyle( 127 | color = colorTheme(light = Color.Black, dark = Color.White) 128 | ) 129 | ) { 130 | append(item.content) 131 | } 132 | 133 | withStyle( 134 | style = SpanStyle( 135 | color = MaterialTheme.colorScheme.primary 136 | ) 137 | ) { 138 | append(item.action) 139 | } 140 | } 141 | ClickableText( 142 | text = annotatedText, 143 | onClick = { offset -> 144 | annotatedText.getStringAnnotations( 145 | tag = "avatar", 146 | start = offset, 147 | end = offset 148 | ).firstOrNull()?.let { 149 | avatarClick() 150 | } 151 | } 152 | ) 153 | } 154 | } 155 | 156 | Divider(modifier = Modifier) 157 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/notice/interactive/InteractiveViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.notice.interactive 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.paging.PagingConfig 8 | import androidx.paging.PagingData 9 | import com.eternaljust.msea.utils.HTMLURL 10 | import com.eternaljust.msea.utils.NetworkUtil 11 | import com.eternaljust.msea.utils.configPager 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.flow.Flow 14 | import kotlinx.coroutines.withContext 15 | import java.util.UUID 16 | 17 | class InteractiveViewModel : ViewModel() { 18 | private val pager by lazy { 19 | configPager(PagingConfig(pageSize = 30, prefetchDistance = 1)) { 20 | loadData(page = it) 21 | } 22 | } 23 | 24 | var viewStates by mutableStateOf(InteractiveFriendListViewState(pagingData = pager)) 25 | private set 26 | 27 | private suspend fun loadData(page: Int) : List { 28 | val list = mutableListOf() 29 | 30 | withContext(Dispatchers.IO) { 31 | val url = HTMLURL.INTERACTIVE_LIST + "&page=${page}" 32 | val document = NetworkUtil.getRequest(url) 33 | val dl = document.selectXpath("//div[@class='nts']/dl") 34 | 35 | dl.forEach { 36 | val friend = InteractiveFriendListModel() 37 | val time = it.selectXpath("dt/span[@class='xg1 xw0']").text() 38 | if (time.isNotEmpty()) { 39 | friend.time = time 40 | } 41 | val avatar = it.selectXpath("dd[@class='m avt mbn']/a/img").attr("src") 42 | if (avatar.isNotEmpty()) { 43 | friend.avatar = NetworkUtil.getAvatar(avatar) 44 | } 45 | val name = it.selectXpath("dd[@class='ntc_body']/a[1]").text() 46 | if (name.isNotEmpty()) { 47 | friend.name = name 48 | } 49 | val href = it.selectXpath("dd[@class='ntc_body']/a[1]").attr("href") 50 | if (href.isNotEmpty()) { 51 | friend.uid = NetworkUtil.getUid(href) 52 | } 53 | val action = it.selectXpath("dd[@class='ntc_body']/a[2]").text() 54 | if (action.isNotEmpty()) { 55 | friend.action = action 56 | } 57 | val text = it.selectXpath("dd[@class='ntc_body']/a[2]").attr("href") 58 | if (text.isNotEmpty()) { 59 | friend.actionURL = text 60 | } 61 | var content = it.selectXpath("dd[@class='ntc_body']").text() 62 | if (content.isNotEmpty()) { 63 | content = content.replace(friend.name, "") 64 | content = content.replace(friend.action, "") 65 | content = content.replace("\r\n", "") 66 | friend.content = content 67 | } 68 | 69 | list.add(friend) 70 | } 71 | } 72 | 73 | return list 74 | } 75 | } 76 | 77 | data class InteractiveFriendListViewState( 78 | val pagingData: Flow> 79 | ) 80 | 81 | class InteractiveFriendListModel { 82 | val uuid = UUID.randomUUID() 83 | var uid = "" 84 | var avatar = "" 85 | var name = "" 86 | var time = "" 87 | var content = "" 88 | var action = "" 89 | var actionURL = "" 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/notice/post/MyPostViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.notice.post 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.paging.* 8 | import com.eternaljust.msea.utils.HTMLURL 9 | import com.eternaljust.msea.utils.NetworkUtil 10 | import com.eternaljust.msea.utils.configPager 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.withContext 14 | import java.util.UUID 15 | 16 | class MyPostViewModel : ViewModel() { 17 | private val pager by lazy { 18 | configPager(PagingConfig(pageSize = 30, prefetchDistance = 1)) { 19 | loadData(page = it) 20 | } 21 | } 22 | 23 | var viewStates by mutableStateOf(MyPostListViewState(pagingData = pager)) 24 | private set 25 | 26 | private suspend fun loadData(page: Int) : List { 27 | val list = mutableListOf() 28 | 29 | withContext(Dispatchers.IO) { 30 | val url = HTMLURL.MY_POST_LIST + "&page=${page}" 31 | val document = NetworkUtil.getRequest(url) 32 | val dl = document.selectXpath("//dl[@class='cl ']") 33 | 34 | dl.forEach { 35 | val isForum = it.html().contains("您的主题") && it.html().contains("移动到") 36 | val post = PostListModel() 37 | val time = it.selectXpath("dt/span[@class='xg1 xw0']").text() 38 | if (time.isNotEmpty()) { 39 | post.time = time 40 | } 41 | if (isForum) { 42 | val avatar = it.selectXpath("dd[@class='m avt mbn']/img").attr("src") 43 | if (avatar.isNotEmpty()) { 44 | post.avatar = NetworkUtil.getAvatar(avatar) 45 | } 46 | } else { 47 | val avatar = it.selectXpath("dd[@class='m avt mbn']/a/img").attr("src") 48 | if (avatar.isNotEmpty()) { 49 | post.avatar = NetworkUtil.getAvatar(avatar) 50 | } 51 | } 52 | val namePath = if (!isForum) "dd[@class='ntc_body']/a[1]" else 53 | "dd[@class='ntc_body']/a[2]" 54 | val name = it.selectXpath(namePath).text() 55 | if (name.isNotEmpty()) { 56 | post.name = name 57 | } 58 | val href = it.selectXpath(namePath).attr("href") 59 | if (href.contains("uid-")) { 60 | post.uid = NetworkUtil.getUid(href) 61 | } 62 | val threadPath = if (!isForum) "dd[@class='ntc_body']/a[2]" else 63 | "dd[@class='ntc_body']/a[1]" 64 | val title = it.selectXpath(threadPath).text() 65 | if (title.isNotEmpty()) { 66 | post.title = title 67 | } 68 | val thread = it.selectXpath(threadPath).attr("href") 69 | if (thread.isNotEmpty()) { 70 | post.ptid = NetworkUtil.getTid(thread) 71 | } 72 | if (isForum) { 73 | val forumPath = "dd[@class='ntc_body']/a[3]" 74 | val forum = it.selectXpath(forumPath).text() 75 | if (forum.isNotEmpty()) { 76 | post.forum = forum 77 | } 78 | val id = it.selectXpath(forumPath).attr("href") 79 | post.fid = NetworkUtil.getFid(id) 80 | } 81 | 82 | list.add(post) 83 | } 84 | } 85 | 86 | return list 87 | } 88 | } 89 | 90 | data class MyPostListViewState( 91 | val pagingData: Flow> 92 | ) 93 | 94 | class PostListModel { 95 | val uuid = UUID.randomUUID() 96 | var fid = "" 97 | var ptid = "" 98 | var uid = "" 99 | var avatar = "" 100 | var name = "" 101 | var time = "" 102 | var title = "" 103 | var forum = "" 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/notice/system/SystemPage.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.notice.system 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material3.* 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.res.painterResource 9 | import androidx.compose.ui.text.font.FontWeight 10 | import androidx.compose.ui.unit.dp 11 | import androidx.lifecycle.viewmodel.compose.viewModel 12 | import androidx.navigation.NavHostController 13 | import androidx.paging.compose.collectAsLazyPagingItems 14 | import androidx.paging.compose.itemKey 15 | import com.eternaljust.msea.R 16 | import com.eternaljust.msea.ui.widget.RefreshList 17 | 18 | @Composable 19 | fun SystemPage( 20 | scaffoldState: SnackbarHostState, 21 | navController: NavHostController, 22 | viewModel: SystemViewModel = viewModel() 23 | ) { 24 | val viewStates = viewModel.viewStates 25 | val lazyPagingItems = viewStates.pagingData.collectAsLazyPagingItems() 26 | 27 | RefreshList( 28 | lazyPagingItems = lazyPagingItems 29 | ) { 30 | items( 31 | count = lazyPagingItems.itemCount, 32 | key = lazyPagingItems.itemKey { it.uuid }, 33 | ) { index -> 34 | val item = lazyPagingItems[index] 35 | item?.let { 36 | SystemListItemContent(it) 37 | } 38 | } 39 | } 40 | } 41 | 42 | @Composable 43 | fun SystemListItemContent(item: SystemListModel) { 44 | Row( 45 | modifier = Modifier 46 | .fillMaxWidth() 47 | .padding(vertical = 10.dp), 48 | verticalAlignment = Alignment.CenterVertically 49 | ) { 50 | Icon( 51 | painter = painterResource(id = R.drawable.ic_baseline_feed_24), 52 | contentDescription = null 53 | ) 54 | 55 | Spacer(modifier = Modifier.width(10.dp)) 56 | 57 | Column( 58 | verticalArrangement = Arrangement.Center 59 | ) { 60 | Text( 61 | text = item.time, 62 | style = MaterialTheme.typography.labelMedium, 63 | fontWeight = FontWeight.Normal 64 | ) 65 | 66 | Text( 67 | text = item.content 68 | ) 69 | } 70 | } 71 | 72 | Divider(modifier = Modifier) 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/notice/system/SystemViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.notice.system 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.paging.* 8 | import com.eternaljust.msea.utils.HTMLURL 9 | import com.eternaljust.msea.utils.NetworkUtil 10 | import com.eternaljust.msea.utils.configPager 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.withContext 14 | import java.util.UUID 15 | 16 | class SystemViewModel : ViewModel() { 17 | private val pager by lazy { 18 | configPager(PagingConfig(pageSize = 30, prefetchDistance = 1)) { 19 | loadData(page = it) 20 | } 21 | } 22 | 23 | var viewStates by mutableStateOf(SystemListViewState(pagingData = pager)) 24 | private set 25 | 26 | private suspend fun loadData(page: Int) : List { 27 | val list = mutableListOf() 28 | 29 | withContext(Dispatchers.IO) { 30 | val url = HTMLURL.SYSTEM_LIST + "&page=${page}" 31 | val document = NetworkUtil.getRequest(url) 32 | val dl = document.selectXpath("//div[@class='nts']/dl") 33 | 34 | dl.forEach { 35 | val system = SystemListModel() 36 | val time = it.selectXpath("dt/span[@class='xg1 xw0']").text() 37 | if (time.isNotEmpty()) { 38 | system.time = time 39 | } 40 | val content = it.selectXpath("dd[@class='ntc_body']").text() 41 | if (content.isNotEmpty()) { 42 | system.content = content 43 | } 44 | 45 | list.add(system) 46 | } 47 | } 48 | 49 | return list 50 | } 51 | } 52 | 53 | data class SystemListViewState( 54 | val pagingData: Flow> 55 | ) 56 | 57 | class SystemListModel { 58 | val uuid = UUID.randomUUID() 59 | var time = "" 60 | var content = "" 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/profile/detail/ProfileCreditPage.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.profile.detail 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.LaunchedEffect 8 | import androidx.compose.runtime.rememberCoroutineScope 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.platform.LocalContext 11 | import androidx.compose.ui.unit.dp 12 | import androidx.lifecycle.viewmodel.compose.viewModel 13 | import androidx.navigation.NavHostController 14 | import com.eternaljust.msea.R 15 | import com.eternaljust.msea.ui.widget.NormalTopAppBar 16 | import com.eternaljust.msea.utils.StatisticsTool 17 | import com.google.accompanist.pager.ExperimentalPagerApi 18 | import com.google.accompanist.pager.HorizontalPager 19 | import com.google.accompanist.pager.rememberPagerState 20 | import kotlinx.coroutines.launch 21 | 22 | @OptIn(ExperimentalPagerApi::class, ExperimentalMaterial3Api::class) 23 | @Composable 24 | fun ProfileCreditPage( 25 | scaffoldState: SnackbarHostState, 26 | navController: NavHostController, 27 | viewModel: ProfileCreditViewModel = viewModel() 28 | ) { 29 | val context = LocalContext.current 30 | 31 | LaunchedEffect(Unit) { 32 | viewModel.viewEvents.collect { 33 | when (it) { 34 | is ProfileCreditViewEvent.PopBack -> { 35 | navController.popBackStack() 36 | } 37 | } 38 | } 39 | } 40 | 41 | Scaffold( 42 | topBar = { 43 | NormalTopAppBar( 44 | title = "我的积分", 45 | onClick = { viewModel.dispatch(ProfileCreditViewAction.PopBack) } 46 | ) 47 | }, 48 | content = { paddingValues -> 49 | Surface( 50 | modifier = Modifier 51 | .padding(paddingValues) 52 | .padding(horizontal = 16.dp) 53 | ) { 54 | val pagerState = rememberPagerState() 55 | val scope = rememberCoroutineScope() 56 | val items = viewModel.items 57 | 58 | Column { 59 | TabRow(selectedTabIndex = pagerState.currentPage) { 60 | items.forEachIndexed { index, item -> 61 | Tab( 62 | text = { Text(item.title) }, 63 | selected = pagerState.currentPage == index, 64 | onClick = { 65 | scope.launch { 66 | pagerState.scrollToPage(index) 67 | } 68 | StatisticsTool.instance.eventObject( 69 | context = context, 70 | resId = R.string.event_page_tab, 71 | keyAndValue = mapOf( 72 | R.string.key_name_credit to item.title 73 | ) 74 | ) 75 | } 76 | ) 77 | } 78 | } 79 | 80 | HorizontalPager(count = items.size, state = pagerState) { 81 | if (it == pagerState.currentPage) { 82 | when (items[pagerState.currentPage]) { 83 | ProfileCreditTabItem.LOG -> CreditLogPage( 84 | scaffoldState = scaffoldState, 85 | navController = navController 86 | ) 87 | ProfileCreditTabItem.SYSTEM -> CreditSystemPage( 88 | scaffoldState = scaffoldState, 89 | navController = navController 90 | ) 91 | ProfileCreditTabItem.RULE -> CreditRulePage( 92 | scaffoldState = scaffoldState, 93 | navController = navController 94 | ) 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/profile/detail/ProfileCreditViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.profile.detail 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.channels.Channel 6 | import kotlinx.coroutines.flow.receiveAsFlow 7 | import kotlinx.coroutines.launch 8 | 9 | class ProfileCreditViewModel : ViewModel() { 10 | val items: List 11 | get() = listOf( 12 | ProfileCreditTabItem.LOG, 13 | ProfileCreditTabItem.SYSTEM, 14 | ProfileCreditTabItem.RULE 15 | ) 16 | 17 | private val _viewEvents = Channel(Channel.BUFFERED) 18 | val viewEvents = _viewEvents.receiveAsFlow() 19 | 20 | fun dispatch(action: ProfileCreditViewAction) { 21 | when (action) { 22 | is ProfileCreditViewAction.PopBack -> popBack() 23 | } 24 | } 25 | 26 | private fun popBack() { 27 | viewModelScope.launch { 28 | _viewEvents.send(ProfileCreditViewEvent.PopBack) 29 | } 30 | } 31 | } 32 | 33 | sealed class ProfileCreditViewEvent { 34 | object PopBack : ProfileCreditViewEvent() 35 | } 36 | 37 | sealed class ProfileCreditViewAction { 38 | object PopBack: ProfileCreditViewAction() 39 | } 40 | 41 | interface ProfileCreditTab { 42 | val id: String 43 | val title: String 44 | } 45 | 46 | enum class ProfileCreditTabItem : ProfileCreditTab { 47 | LOG { 48 | override val id: String 49 | get() = "log" 50 | 51 | override val title: String 52 | get() = "积分收益" 53 | }, 54 | 55 | SYSTEM { 56 | override val id: String 57 | get() = "system" 58 | 59 | override val title: String 60 | get() = "系统奖励" 61 | }, 62 | 63 | RULE { 64 | override val id: String 65 | get() = "rule" 66 | 67 | override val title: String 68 | get() = "积分规则" 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/profile/detail/ProfileFavoriteViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.profile.detail 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import androidx.paging.PagingConfig 9 | import androidx.paging.PagingData 10 | import com.eternaljust.msea.utils.HTMLURL 11 | import com.eternaljust.msea.utils.NetworkUtil 12 | import com.eternaljust.msea.utils.configPager 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.channels.Channel 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.receiveAsFlow 17 | import kotlinx.coroutines.launch 18 | import kotlinx.coroutines.withContext 19 | import java.util.UUID 20 | 21 | class ProfileFavoriteViewModel : ViewModel() { 22 | private val pager by lazy { 23 | configPager(PagingConfig(pageSize = 30, prefetchDistance = 1)) { 24 | loadData(page = it) 25 | } 26 | } 27 | 28 | var viewStates by mutableStateOf(ProfileFavoriteListViewState(pagingData = pager)) 29 | private set 30 | 31 | private val _viewEvents = Channel(Channel.BUFFERED) 32 | val viewEvents = _viewEvents.receiveAsFlow() 33 | 34 | fun dispatch(action: ProfileFavoriteListViewAction) { 35 | when (action) { 36 | is ProfileFavoriteListViewAction.PopBack -> popBack() 37 | is ProfileFavoriteListViewAction.Delete -> delete() 38 | is ProfileFavoriteListViewAction.DeleteAction -> { 39 | viewStates = viewStates.copy(action = action.action) 40 | } 41 | is ProfileFavoriteListViewAction.DeleteShowDialog -> showDeleteDialog(action.isShow) 42 | } 43 | } 44 | 45 | private fun delete() { 46 | viewModelScope.launch(Dispatchers.IO) { 47 | val url = HTMLURL.BASE + "/${viewStates.action}" 48 | val document = NetworkUtil.postRequest(url, emptyMap()) 49 | val result = document.html() 50 | if (result.isNotEmpty()) { 51 | _viewEvents.send(ProfileFavoriteListViewEvent.Message("删除成功")) 52 | _viewEvents.send(ProfileFavoriteListViewEvent.Refresh) 53 | } else { 54 | _viewEvents.send(ProfileFavoriteListViewEvent.Message("删除失败,请稍后重试")) 55 | } 56 | showDeleteDialog(isShow = false) 57 | } 58 | } 59 | 60 | private fun showDeleteDialog(isShow: Boolean) { 61 | viewStates = viewStates.copy(showDeleteDialog = isShow) 62 | } 63 | 64 | private suspend fun loadData(page: Int) : List { 65 | val list = mutableListOf() 66 | 67 | withContext(Dispatchers.IO) { 68 | val url = HTMLURL.PROFILE_FAVORITE_LIST + "&page=${page}" 69 | val document = NetworkUtil.getRequest(url) 70 | val li = document.selectXpath("//ul[@id='favorite_ul']/li") 71 | 72 | li.forEach { 73 | println("li---${it.html()}") 74 | val system = ProfileFavoriteListModel() 75 | val time = it.selectXpath("span[@class='xg1']").text() 76 | if (time.isNotEmpty()) { 77 | system.time = time 78 | } 79 | val title = it.selectXpath("a[last()]").text() 80 | if (title.isNotEmpty()) { 81 | system.title = title 82 | } 83 | val tid = it.selectXpath("a[last()]").attr("href") 84 | if (tid.contains("thread-")) { 85 | system.tid = tid.split("thread-").last() 86 | .split("-").first() 87 | } 88 | val action = it.selectXpath("a[1]").attr("href") 89 | if (action.isNotEmpty()) { 90 | system.action = action 91 | } 92 | val id = it.selectXpath("a[1]").attr("id") 93 | if (id.isNotEmpty()) { 94 | system.action = system.action + "&deletesubmit=true&handlekey=$id" 95 | } 96 | println("action---${system.action}") 97 | 98 | list.add(system) 99 | } 100 | } 101 | 102 | return list 103 | } 104 | 105 | private fun popBack() { 106 | viewModelScope.launch { 107 | _viewEvents.send(ProfileFavoriteListViewEvent.PopBack) 108 | } 109 | } 110 | } 111 | 112 | data class ProfileFavoriteListViewState( 113 | val pagingData: Flow>, 114 | val showDeleteDialog: Boolean = false, 115 | val action: String = "" 116 | ) 117 | 118 | sealed class ProfileFavoriteListViewEvent { 119 | object PopBack : ProfileFavoriteListViewEvent() 120 | object Refresh : ProfileFavoriteListViewEvent() 121 | 122 | data class Message(val message: String) : ProfileFavoriteListViewEvent() 123 | } 124 | 125 | sealed class ProfileFavoriteListViewAction { 126 | object PopBack: ProfileFavoriteListViewAction() 127 | object Delete: ProfileFavoriteListViewAction() 128 | 129 | data class DeleteAction(val action: String) : ProfileFavoriteListViewAction() 130 | data class DeleteShowDialog(val isShow: Boolean) : ProfileFavoriteListViewAction() 131 | } 132 | 133 | class ProfileFavoriteListModel { 134 | val uuid = UUID.randomUUID() 135 | var time = "" 136 | var title = "" 137 | var tid = "" 138 | var action = "" 139 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/profile/detail/ProfileFirendViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.profile.detail 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.channels.Channel 6 | import kotlinx.coroutines.flow.receiveAsFlow 7 | import kotlinx.coroutines.launch 8 | 9 | class ProfileFriendViewModel : ViewModel() { 10 | val items: List 11 | get() = listOf( 12 | ProfileFriendTabItem.FRIEND, 13 | ProfileFriendTabItem.VISITOR, 14 | ProfileFriendTabItem.TRACE 15 | ) 16 | 17 | private val _viewEvents = Channel(Channel.BUFFERED) 18 | val viewEvents = _viewEvents.receiveAsFlow() 19 | 20 | fun dispatch(action: ProfileFriendViewAction) { 21 | when (action) { 22 | is ProfileFriendViewAction.PopBack -> popBack() 23 | } 24 | } 25 | 26 | private fun popBack() { 27 | viewModelScope.launch { 28 | _viewEvents.send(ProfileFriendViewEvent.PopBack) 29 | } 30 | } 31 | } 32 | 33 | sealed class ProfileFriendViewEvent { 34 | object PopBack : ProfileFriendViewEvent() 35 | } 36 | 37 | sealed class ProfileFriendViewAction { 38 | object PopBack: ProfileFriendViewAction() 39 | } 40 | 41 | interface ProfileFriendTab { 42 | val id: String 43 | val title: String 44 | val header: String 45 | } 46 | 47 | enum class ProfileFriendTabItem : ProfileFriendTab { 48 | FRIEND { 49 | override val id: String 50 | get() = "friend" 51 | 52 | override val title: String 53 | get() = "好友列表" 54 | 55 | override val header: String 56 | get() = "按照好友热度排序" 57 | }, 58 | 59 | VISITOR { 60 | override val id: String 61 | get() = "visitor" 62 | 63 | override val title: String 64 | get() = "我的访客" 65 | 66 | override val header: String 67 | get() = "他们拜访过您,回访一下吧" 68 | }, 69 | 70 | TRACE { 71 | override val id: String 72 | get() = "trace" 73 | 74 | override val title: String 75 | get() = "我的足迹" 76 | 77 | override val header: String 78 | get() = "您曾经拜访过的用户列表" 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/profile/detail/ProfileFriendListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.profile.detail 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.paging.PagingConfig 8 | import androidx.paging.PagingData 9 | import com.eternaljust.msea.utils.HTMLURL 10 | import com.eternaljust.msea.utils.NetworkUtil 11 | import com.eternaljust.msea.utils.UserInfo 12 | import com.eternaljust.msea.utils.configPager 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.withContext 16 | import java.util.UUID 17 | 18 | class FriendListViewModel : ViewModel() { 19 | private val pager by lazy { 20 | configPager(PagingConfig(pageSize = 30, prefetchDistance = 1)) { 21 | loadData(page = it) 22 | } 23 | } 24 | 25 | var viewStates by mutableStateOf(FriendListViewState(pagingData = pager)) 26 | private set 27 | 28 | private suspend fun loadData(page: Int) : List { 29 | val list = mutableListOf() 30 | 31 | withContext(Dispatchers.IO) { 32 | val url = HTMLURL.FRIEND_LIST + "&order=num&page=${page}" 33 | val document = NetworkUtil.getRequest(url) 34 | val count = document.selectXpath("//div[@class='tbmu cl']/p/span[@class='xw1']").text() 35 | if (count.isNotEmpty()) { 36 | viewStates = viewStates.copy(count = count) 37 | } 38 | val li = document.selectXpath("//ul[@class='buddy cl']/li") 39 | 40 | li.forEach { 41 | val friend = FriendListModel() 42 | val avatar = it.selectXpath("div[@class='avt']/a/img").attr("src") 43 | if (avatar.isNotEmpty()) { 44 | friend.avatar = NetworkUtil.getAvatar(avatar) 45 | } 46 | val name = it.selectXpath("h4/a").text() 47 | if (name.isNotEmpty()) { 48 | friend.name = name 49 | } 50 | val hot = it.selectXpath("h4/span[@class='xg1 xw0 y']").text() 51 | if (hot.isNotEmpty()) { 52 | friend.hot = hot.replace("\n", "") 53 | } 54 | val uid = it.selectXpath("h4/a").attr("href") 55 | if (uid.contains("uid-")) { 56 | friend.uid = NetworkUtil.getUid(uid) 57 | } 58 | val topic = it.selectXpath("p[@class='maxh']").text() 59 | if (topic.isNotEmpty()) { 60 | friend.topic = topic 61 | } 62 | 63 | list.add(friend) 64 | } 65 | } 66 | 67 | return list 68 | } 69 | } 70 | 71 | data class FriendListViewState( 72 | val pagingData: Flow>, 73 | val count: String = "0" 74 | ) 75 | 76 | class FriendListModel { 77 | val uuid = UUID.randomUUID() 78 | var name = "" 79 | var uid = "" 80 | var avatar = "" 81 | var hot = "" 82 | var topic = "" 83 | } 84 | 85 | class FriendVisitorTraceListViewModel( 86 | val tabItem: ProfileFriendTabItem 87 | ) : ViewModel() { 88 | companion object { 89 | val visitor by lazy { FriendVisitorTraceListViewModel(tabItem = ProfileFriendTabItem.VISITOR) } 90 | val trace by lazy { FriendVisitorTraceListViewModel(tabItem = ProfileFriendTabItem.TRACE) } 91 | } 92 | 93 | private val pager by lazy { 94 | configPager(PagingConfig(pageSize = 30, prefetchDistance = 1)) { 95 | loadData(page = it) 96 | } 97 | } 98 | 99 | var viewStates by mutableStateOf(FriendVisitorTraceListViewState(pagingData = pager)) 100 | private set 101 | 102 | private suspend fun loadData(page: Int) : List { 103 | val list = mutableListOf() 104 | 105 | withContext(Dispatchers.IO) { 106 | val url = HTMLURL.FRIEND_LIST + "&uid=${UserInfo.instance.uid}&view=${tabItem.id}&page=${page}" 107 | val document = NetworkUtil.getRequest(url) 108 | val li = document.selectXpath("//ul[@class='buddy cl']/li") 109 | 110 | li.forEach { 111 | val friend = FriendVisitorTraceListModel() 112 | val avatar = it.selectXpath("div[@class='avt']/a/img").attr("src") 113 | if (avatar.isNotEmpty()) { 114 | friend.avatar = NetworkUtil.getAvatar(avatar) 115 | } 116 | val name = it.selectXpath("h4/a").text() 117 | if (name.isNotEmpty()) { 118 | friend.name = name 119 | } 120 | val time = it.selectXpath("h4/span[@class='xg1 xw0 y']").text() 121 | if (time.isNotEmpty()) { 122 | friend.time = time.replace("\n", "") 123 | } 124 | val uid = it.selectXpath("h4/a").attr("href") 125 | if (uid.contains("uid-")) { 126 | friend.uid = NetworkUtil.getUid(uid) 127 | } 128 | val topic = it.selectXpath("p[@class='maxh']").text() 129 | if (topic.isNotEmpty()) { 130 | friend.topic = topic 131 | } 132 | 133 | list.add(friend) 134 | } 135 | } 136 | 137 | return list 138 | } 139 | } 140 | 141 | data class FriendVisitorTraceListViewState( 142 | val pagingData: Flow>, 143 | ) 144 | 145 | class FriendVisitorTraceListModel { 146 | val uuid = UUID.randomUUID() 147 | var name = "" 148 | var uid = "" 149 | var avatar = "" 150 | var time = "" 151 | var topic = "" 152 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/profile/detail/ProfileFriendPage.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.profile.detail 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.LaunchedEffect 8 | import androidx.compose.runtime.rememberCoroutineScope 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.platform.LocalContext 11 | import androidx.compose.ui.unit.dp 12 | import androidx.lifecycle.viewmodel.compose.viewModel 13 | import androidx.navigation.NavHostController 14 | import com.eternaljust.msea.R 15 | import com.eternaljust.msea.ui.widget.NormalTopAppBar 16 | import com.eternaljust.msea.utils.StatisticsTool 17 | import com.google.accompanist.pager.ExperimentalPagerApi 18 | import com.google.accompanist.pager.HorizontalPager 19 | import com.google.accompanist.pager.rememberPagerState 20 | import kotlinx.coroutines.launch 21 | 22 | @OptIn(ExperimentalPagerApi::class, ExperimentalMaterial3Api::class) 23 | @Composable 24 | fun ProfileFriendPage( 25 | scaffoldState: SnackbarHostState, 26 | navController: NavHostController, 27 | viewModel: ProfileFriendViewModel = viewModel() 28 | ) { 29 | val context = LocalContext.current 30 | 31 | LaunchedEffect(Unit) { 32 | viewModel.viewEvents.collect { 33 | when (it) { 34 | is ProfileFriendViewEvent.PopBack -> { 35 | navController.popBackStack() 36 | } 37 | } 38 | } 39 | } 40 | 41 | Scaffold( 42 | topBar = { 43 | NormalTopAppBar( 44 | title = "我的好友", 45 | onClick = { viewModel.dispatch(ProfileFriendViewAction.PopBack) } 46 | ) 47 | }, 48 | content = { paddingValues -> 49 | Surface( 50 | modifier = Modifier 51 | .padding(paddingValues) 52 | .padding(horizontal = 16.dp) 53 | ) { 54 | val pagerState = rememberPagerState() 55 | val scope = rememberCoroutineScope() 56 | val items = viewModel.items 57 | 58 | Column { 59 | TabRow(selectedTabIndex = pagerState.currentPage) { 60 | items.forEachIndexed { index, item -> 61 | Tab( 62 | text = { Text(item.title) }, 63 | selected = pagerState.currentPage == index, 64 | onClick = { 65 | scope.launch { 66 | pagerState.scrollToPage(index) 67 | } 68 | StatisticsTool.instance.eventObject( 69 | context = context, 70 | resId = R.string.event_page_tab, 71 | keyAndValue = mapOf( 72 | R.string.key_name_friend to item.title 73 | ) 74 | ) 75 | } 76 | ) 77 | } 78 | } 79 | 80 | HorizontalPager(count = items.size, state = pagerState) { 81 | if (it == pagerState.currentPage) { 82 | when (val item = items[pagerState.currentPage]) { 83 | ProfileFriendTabItem.FRIEND -> FriendListPage( 84 | scaffoldState = scaffoldState, 85 | navController = navController 86 | ) 87 | else -> FriendVisitorTraceListPage( 88 | scaffoldState = scaffoldState, 89 | navController = navController, 90 | viewModel = if (item == ProfileFriendTabItem.VISITOR) 91 | FriendVisitorTraceListViewModel.visitor else 92 | FriendVisitorTraceListViewModel.trace 93 | ) 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/profile/detail/ProfileGroupPage.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.profile.detail 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.Button 6 | import androidx.compose.material3.SnackbarHostState 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.rememberCoroutineScope 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import androidx.navigation.NavHostController 14 | import kotlinx.coroutines.launch 15 | 16 | @Composable 17 | fun ProfileGroupPage( 18 | scaffoldState: SnackbarHostState, 19 | navController: NavHostController 20 | ) { 21 | val scope = rememberCoroutineScope() 22 | val text = "用户组" 23 | 24 | Surface( 25 | modifier = Modifier 26 | .padding(horizontal = 16.dp) 27 | ) { 28 | Column { 29 | Text(text = text) 30 | 31 | Button(onClick = { 32 | scope.launch { 33 | scaffoldState.showSnackbar(message = text) 34 | } 35 | }) { 36 | Text(text) 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/profile/detail/ProfileTopicViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.profile.detail 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import androidx.paging.PagingConfig 9 | import androidx.paging.PagingData 10 | import com.eternaljust.msea.utils.HTMLURL 11 | import com.eternaljust.msea.utils.NetworkUtil 12 | import com.eternaljust.msea.utils.configPager 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.channels.Channel 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.receiveAsFlow 17 | import kotlinx.coroutines.launch 18 | import kotlinx.coroutines.withContext 19 | import java.util.UUID 20 | 21 | class ProfileTopicViewModel : ViewModel() { 22 | private var uid = "" 23 | 24 | private val pager by lazy { 25 | configPager(PagingConfig(pageSize = 30, prefetchDistance = 1)) { 26 | loadData(page = it) 27 | } 28 | } 29 | 30 | var viewStates by mutableStateOf(ProfileTopicViewState(pagingData = pager)) 31 | private set 32 | 33 | private val _viewEvents = Channel(Channel.BUFFERED) 34 | val viewEvents = _viewEvents.receiveAsFlow() 35 | 36 | fun dispatch(action: ProfileTopicViewAction) { 37 | when (action) { 38 | is ProfileTopicViewAction.PopBack -> popBack() 39 | is ProfileTopicViewAction.SetUid -> uid = action.uid 40 | } 41 | } 42 | 43 | private suspend fun loadData(page: Int) : List { 44 | val list = mutableListOf() 45 | 46 | withContext(Dispatchers.IO) { 47 | val url = HTMLURL.PROFILE_TOPIC_LIST + "&uid=$uid&page=${page}" 48 | val document = NetworkUtil.getRequest(url) 49 | val tr = document.selectXpath("//div[@class='bm_c']//table/tbody/tr") 50 | tr.forEach { 51 | val topic = ProfileTopicListModel() 52 | 53 | val gif = it.selectXpath("td[@class='icn']/a/img").attr("src") 54 | if (gif.isNotEmpty()) { 55 | topic.gif = HTMLURL.BASE + "/${gif}" 56 | } 57 | val title = it.selectXpath("th/a").text() 58 | if (title.isNotEmpty()) { 59 | topic.title = title 60 | } 61 | val tid = it.selectXpath("th/a").attr("href") 62 | if (tid.contains("thread-")) { 63 | topic.tid = tid.split("thread-").last().split("-").first() 64 | } 65 | val forum = it.selectXpath("td/a[@class='xg1']").text() 66 | if (forum.isNotEmpty()) { 67 | topic.forum = forum 68 | } 69 | val fid = it.selectXpath("td/a[@class='xg1']").attr("href") 70 | if (fid.isNotEmpty()) { 71 | topic.fid = NetworkUtil.getFid(fid) 72 | } 73 | val reply = it.selectXpath("td[@class='num']/a").text() 74 | if (reply.isNotEmpty()) { 75 | topic.reply = reply 76 | } 77 | val examine = it.selectXpath("td[@class='num']/em").text() 78 | if (examine.isNotEmpty()) { 79 | topic.examine = examine 80 | } 81 | val lastName = it.selectXpath("td[@class='by'][last()]/cite/a").text() 82 | if (lastName.isNotEmpty()) { 83 | topic.lastName = lastName 84 | } 85 | val lastTime = it.selectXpath("td[@class='by'][last()]/em/a").text() 86 | if (lastTime.isNotEmpty()) { 87 | topic.lastTime = lastTime 88 | } 89 | 90 | if (topic.lastName.isNotEmpty()) { 91 | list.add(topic) 92 | } 93 | } 94 | } 95 | 96 | return list 97 | } 98 | 99 | private fun popBack() { 100 | viewModelScope.launch { 101 | _viewEvents.send(ProfileTopicViewEvent.PopBack) 102 | } 103 | } 104 | } 105 | 106 | data class ProfileTopicViewState( 107 | val pagingData: Flow> 108 | ) 109 | 110 | sealed class ProfileTopicViewEvent { 111 | object PopBack : ProfileTopicViewEvent() 112 | } 113 | 114 | sealed class ProfileTopicViewAction { 115 | object PopBack: ProfileTopicViewAction() 116 | 117 | data class SetUid(val uid: String) : ProfileTopicViewAction() 118 | } 119 | 120 | class ProfileTopicListModel { 121 | val uuid = UUID.randomUUID() 122 | /** 123 | * 帖子标题 124 | */ 125 | var title = "" 126 | /** 127 | * 帖子链接 id 128 | */ 129 | var tid = "" 130 | /** 131 | * 帖子动图 132 | */ 133 | var gif = "" 134 | /** 135 | * 帖子板块 136 | */ 137 | var forum = "" 138 | /** 139 | * 帖子板块 id 140 | */ 141 | var fid = "" 142 | /** 143 | * 查看 144 | */ 145 | var examine = "" 146 | /** 147 | * 回复 148 | */ 149 | var reply = "" 150 | /** 151 | * 最后发表的昵称 152 | */ 153 | var lastName = "" 154 | /** 155 | * 最后发表的时间 156 | */ 157 | var lastTime = "" 158 | } 159 | -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/profile/setting/AboutViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.profile.setting 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.eternaljust.msea.BuildConfig 9 | import com.eternaljust.msea.ui.data.ConfigVersionModel 10 | import com.eternaljust.msea.utils.* 11 | import com.umeng.cconfig.UMRemoteConfig 12 | import kotlinx.coroutines.channels.Channel 13 | import kotlinx.coroutines.flow.receiveAsFlow 14 | import kotlinx.coroutines.launch 15 | 16 | class AboutViewModel : ViewModel() { 17 | val items: List 18 | get() = listOf( 19 | AboutListItem.LICENSE, 20 | AboutListItem.SDK_LIST, 21 | AboutListItem.SOURCE_CODE, 22 | AboutListItem.UPDATE_VERSION 23 | ) 24 | val versionName: String = BuildConfig.VERSION_NAME 25 | val versionCode: Int = BuildConfig.VERSION_CODE 26 | 27 | var viewStates by mutableStateOf(AboutViewStates()) 28 | private set 29 | private val _viewEvents = Channel(Channel.BUFFERED) 30 | val viewEvents = _viewEvents.receiveAsFlow() 31 | 32 | fun dispatch(action: AboutViewAction) { 33 | when (action) { 34 | is AboutViewAction.PopBack -> popBack() 35 | is AboutViewAction.GetVersion -> getVersion() 36 | is AboutViewAction.VersionShowDialog -> versionShowDialog(action.isShow) 37 | } 38 | } 39 | 40 | private fun popBack() { 41 | viewModelScope.launch { 42 | _viewEvents.send(AboutViewEvent.PopBack) 43 | } 44 | } 45 | 46 | private fun getVersion() { 47 | val configVersion = UMRemoteConfig.getInstance().getConfigValue("config_version") 48 | println("config_version $configVersion") 49 | val version = configVersion.fromJson() 50 | version?.let { 51 | viewStates = viewStates.copy(configVersion = it) 52 | } 53 | } 54 | 55 | private fun versionShowDialog(isShow: Boolean) { 56 | viewStates = viewStates.copy(versionShowDialog = isShow) 57 | } 58 | } 59 | 60 | data class AboutViewStates( 61 | var configVersion: ConfigVersionModel = ConfigVersionModel(), 62 | var versionShowDialog: Boolean = false 63 | ) 64 | 65 | sealed class AboutViewEvent { 66 | object PopBack : AboutViewEvent() 67 | } 68 | 69 | sealed class AboutViewAction { 70 | object PopBack: AboutViewAction() 71 | object GetVersion: AboutViewAction() 72 | 73 | data class VersionShowDialog(val isShow: Boolean) : AboutViewAction() 74 | } 75 | 76 | interface AboutList { 77 | val route: String 78 | val title: String 79 | } 80 | 81 | enum class AboutListItem : AboutList { 82 | LICENSE { 83 | override val route: String 84 | get() = RouteName.LICENSE 85 | 86 | override val title: String 87 | get() = "开源协议" 88 | }, 89 | 90 | SOURCE_CODE { 91 | override val route: String 92 | get() = RouteName.SOURCE_CODE 93 | 94 | override val title: String 95 | get() = "源代码" 96 | }, 97 | 98 | SDK_LIST { 99 | override val route: String 100 | get() = RouteName.SDK_LIST 101 | 102 | override val title: String 103 | get() = "SDK 目录" 104 | }, 105 | 106 | UPDATE_VERSION { 107 | override val route: String 108 | get() = RouteName.UPDATE_VERSION 109 | 110 | override val title: String 111 | get() = "版本更新" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/page/profile/setting/SettingViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.page.profile.setting 2 | 3 | import android.os.Build 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import com.eternaljust.msea.utils.RouteName 10 | import com.eternaljust.msea.utils.SettingInfo 11 | import kotlinx.coroutines.channels.Channel 12 | import kotlinx.coroutines.flow.receiveAsFlow 13 | import kotlinx.coroutines.launch 14 | import java.time.LocalTime 15 | 16 | class SettingViewModel : ViewModel() { 17 | val itemGroups: List> 18 | get() { 19 | val isDynamic = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S 20 | val themeList = mutableListOf( 21 | SettingListItem.DARK_MODE 22 | ) 23 | if (isDynamic) { 24 | themeList.add(SettingListItem.COLOR_SCHEME) 25 | } 26 | return listOf( 27 | themeList, 28 | listOf( 29 | SettingListItem.FEEDBACK, 30 | SettingListItem.CONTACT_US, 31 | SettingListItem.SHARE, 32 | // SettingListItem.CLEAN_CACHE, 33 | ), 34 | listOf( 35 | SettingListItem.TERMS_OF_SERVICE, 36 | SettingListItem.PRIVACY_POLICY, 37 | ) 38 | ) 39 | } 40 | 41 | val themeStyleItems: List 42 | get() = listOf("自动", "浅色", "深色") 43 | 44 | var viewStates by mutableStateOf(SettingViewState()) 45 | private set 46 | private val _viewEvents = Channel(Channel.BUFFERED) 47 | val viewEvents = _viewEvents.receiveAsFlow() 48 | 49 | fun dispatch(action: SettingViewAction) { 50 | when (action) { 51 | is SettingViewAction.PopBack -> popBack() 52 | is SettingViewAction.UpdateContactUsShow-> updateContactUsShow(show = action.show) 53 | is SettingViewAction.UpdateColorSchemeChecked -> updateColorSchemeChecked(check = action.check) 54 | is SettingViewAction.UpdateThemeStyleIndex -> updateThemeStyleIndex(index = action.index) 55 | } 56 | } 57 | 58 | private fun popBack() { 59 | viewModelScope.launch { 60 | _viewEvents.send(SettingViewEvent.PopBack) 61 | } 62 | } 63 | 64 | private fun updateContactUsShow(show: Boolean) { 65 | viewStates = viewStates.copy(isContactUsShow = show) 66 | } 67 | 68 | private fun updateColorSchemeChecked(check: Boolean) { 69 | SettingInfo.instance.colorScheme = check 70 | viewStates = viewStates.copy(colorSchemeChecked = check) 71 | } 72 | 73 | private fun updateThemeStyleIndex(index: Int) { 74 | SettingInfo.instance.themeStyle = index 75 | viewStates = viewStates.copy(themeStyleIndex = index) 76 | } 77 | } 78 | 79 | data class SettingViewState constructor( 80 | val isContactUsShow: Boolean = false, 81 | val colorSchemeChecked: Boolean = SettingInfo.instance.colorScheme, 82 | val themeStyleIndex: Int = SettingInfo.instance.themeStyle 83 | ) 84 | 85 | sealed class SettingViewEvent { 86 | object PopBack : SettingViewEvent() 87 | } 88 | 89 | sealed class SettingViewAction { 90 | object PopBack : SettingViewAction() 91 | 92 | data class UpdateContactUsShow(val show: Boolean) : SettingViewAction() 93 | data class UpdateColorSchemeChecked(val check: Boolean) : SettingViewAction() 94 | data class UpdateThemeStyleIndex(val index: Int) : SettingViewAction() 95 | } 96 | 97 | interface SettingList { 98 | val route: String 99 | val title: String 100 | } 101 | 102 | enum class SettingListItem : SettingList { 103 | DARK_MODE { 104 | override val route: String 105 | get() = "dark_mode" 106 | 107 | override val title: String 108 | get() = "深色模式" 109 | }, 110 | 111 | COLOR_SCHEME { 112 | override val route: String 113 | get() = "color_scheme" 114 | 115 | override val title: String 116 | get() = "主题壁纸动态配色(Android 12 +)" 117 | }, 118 | 119 | FEEDBACK { 120 | override val route: String 121 | get() = "feedback" 122 | 123 | override val title: String 124 | get() = "反馈问题" 125 | }, 126 | 127 | CONTACT_US { 128 | override val route: String 129 | get() = "contact_us" 130 | 131 | override val title: String 132 | get() = "联系我们" 133 | }, 134 | 135 | SHARE { 136 | override val route: String 137 | get() = "share" 138 | 139 | override val title: String 140 | get() = "分享给朋友" 141 | }, 142 | 143 | TERMS_OF_SERVICE { 144 | override val route: String 145 | get() = RouteName.TERMS_OF_SERVICE 146 | 147 | override val title: String 148 | get() = "使用条款" 149 | }, 150 | 151 | PRIVACY_POLICY { 152 | override val route: String 153 | get() = RouteName.PRIVACY_POLICY 154 | 155 | override val title: String 156 | get() = "隐私政策" 157 | } 158 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.graphics.Color 7 | import com.eternaljust.msea.utils.SettingInfo 8 | 9 | val md_theme_light_primary = Color(0xFF006E26) 10 | val md_theme_light_onPrimary = Color(0xFFFFFFFF) 11 | val md_theme_light_primaryContainer = Color(0xFF7AFD8A) 12 | val md_theme_light_onPrimaryContainer = Color(0xFF002106) 13 | val md_theme_light_secondary = Color(0xFF4355B9) 14 | val md_theme_light_onSecondary = Color(0xFFFFFFFF) 15 | val md_theme_light_secondaryContainer = Color(0xFFDEE0FF) 16 | val md_theme_light_onSecondaryContainer = Color(0xFF00105C) 17 | val md_theme_light_tertiary = Color(0xFF855300) 18 | val md_theme_light_onTertiary = Color(0xFFFFFFFF) 19 | val md_theme_light_tertiaryContainer = Color(0xFFFFDDB8) 20 | val md_theme_light_onTertiaryContainer = Color(0xFF2A1700) 21 | val md_theme_light_error = Color(0xFFBA1A1A) 22 | val md_theme_light_errorContainer = Color(0xFFFFDAD6) 23 | val md_theme_light_onError = Color(0xFFFFFFFF) 24 | val md_theme_light_onErrorContainer = Color(0xFF410002) 25 | val md_theme_light_background = Color(0xFFFCFDF7) 26 | val md_theme_light_onBackground = Color(0xFF1A1C19) 27 | val md_theme_light_surface = Color(0xFFFCFDF7) 28 | val md_theme_light_onSurface = Color(0xFF1A1C19) 29 | val md_theme_light_surfaceVariant = Color(0xFFDEE5D9) 30 | val md_theme_light_onSurfaceVariant = Color(0xFF424940) 31 | val md_theme_light_outline = Color(0xFF72796F) 32 | val md_theme_light_inverseOnSurface = Color(0xFFF0F1EB) 33 | val md_theme_light_inverseSurface = Color(0xFF2F312D) 34 | val md_theme_light_inversePrimary = Color(0xFF5CE071) 35 | val md_theme_light_shadow = Color(0xFF000000) 36 | val md_theme_light_surfaceTint = Color(0xFF006E26) 37 | val md_theme_light_surfaceTintColor = Color(0xFF006E26) 38 | 39 | val md_theme_dark_primary = Color(0xFF5CE071) 40 | val md_theme_dark_onPrimary = Color(0xFF003910) 41 | val md_theme_dark_primaryContainer = Color(0xFF00531B) 42 | val md_theme_dark_onPrimaryContainer = Color(0xFF7AFD8A) 43 | val md_theme_dark_secondary = Color(0xFFBAC3FF) 44 | val md_theme_dark_onSecondary = Color(0xFF08218A) 45 | val md_theme_dark_secondaryContainer = Color(0xFF293CA0) 46 | val md_theme_dark_onSecondaryContainer = Color(0xFFDEE0FF) 47 | val md_theme_dark_tertiary = Color(0xFFFFB960) 48 | val md_theme_dark_onTertiary = Color(0xFF472A00) 49 | val md_theme_dark_tertiaryContainer = Color(0xFF653E00) 50 | val md_theme_dark_onTertiaryContainer = Color(0xFFFFDDB8) 51 | val md_theme_dark_error = Color(0xFFFFB4AB) 52 | val md_theme_dark_errorContainer = Color(0xFF93000A) 53 | val md_theme_dark_onError = Color(0xFF690005) 54 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) 55 | val md_theme_dark_background = Color(0xFF1A1C19) 56 | val md_theme_dark_onBackground = Color(0xFFE2E3DD) 57 | val md_theme_dark_surface = Color(0xFF1A1C19) 58 | val md_theme_dark_onSurface = Color(0xFFE2E3DD) 59 | val md_theme_dark_surfaceVariant = Color(0xFF424940) 60 | val md_theme_dark_onSurfaceVariant = Color(0xFFC2C9BD) 61 | val md_theme_dark_outline = Color(0xFF8C9388) 62 | val md_theme_dark_inverseOnSurface = Color(0xFF1A1C19) 63 | val md_theme_dark_inverseSurface = Color(0xFFE2E3DD) 64 | val md_theme_dark_inversePrimary = Color(0xFF006E26) 65 | val md_theme_dark_shadow = Color(0xFF000000) 66 | val md_theme_dark_surfaceTint = Color(0xFF5CE071) 67 | val md_theme_dark_surfaceTintColor = Color(0xFF5CE071) 68 | 69 | 70 | val seed = Color(0xFF53D769) 71 | 72 | @Composable 73 | fun colorTheme( 74 | light: Color, 75 | dark: Color 76 | ): Color { 77 | val themeStyle = SettingInfo.instance.themeStyle 78 | return if (themeStyle == 0) { 79 | if (isSystemInDarkTheme()) dark else light 80 | } else if (themeStyle == 1) { 81 | light 82 | } else { 83 | dark 84 | } 85 | } 86 | 87 | @Composable 88 | fun getIconTintColorSecondary(isNodeFid125: Boolean): Color { 89 | return if (isNodeFid125) Color.Gray else MaterialTheme.colorScheme.secondary 90 | } 91 | 92 | @Composable 93 | fun getIconTintColorPrimary(isNodeFid125: Boolean): Color { 94 | return if (isNodeFid125) Color.Gray else MaterialTheme.colorScheme.primary 95 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.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.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.ViewCompat 17 | import com.eternaljust.msea.utils.SettingInfo 18 | 19 | private val LightColorScheme = lightColorScheme( 20 | primary = md_theme_light_primary, 21 | onPrimary = md_theme_light_onPrimary, 22 | primaryContainer = md_theme_light_primaryContainer, 23 | onPrimaryContainer = md_theme_light_onPrimaryContainer, 24 | secondary = md_theme_light_secondary, 25 | onSecondary = md_theme_light_onSecondary, 26 | secondaryContainer = md_theme_light_secondaryContainer, 27 | onSecondaryContainer = md_theme_light_onSecondaryContainer, 28 | tertiary = md_theme_light_tertiary, 29 | onTertiary = md_theme_light_onTertiary, 30 | tertiaryContainer = md_theme_light_tertiaryContainer, 31 | onTertiaryContainer = md_theme_light_onTertiaryContainer, 32 | error = md_theme_light_error, 33 | errorContainer = md_theme_light_errorContainer, 34 | onError = md_theme_light_onError, 35 | onErrorContainer = md_theme_light_onErrorContainer, 36 | background = md_theme_light_background, 37 | onBackground = md_theme_light_onBackground, 38 | surface = md_theme_light_surface, 39 | onSurface = md_theme_light_onSurface, 40 | surfaceVariant = md_theme_light_surfaceVariant, 41 | onSurfaceVariant = md_theme_light_onSurfaceVariant, 42 | outline = md_theme_light_outline, 43 | inverseOnSurface = md_theme_light_inverseOnSurface, 44 | inverseSurface = md_theme_light_inverseSurface, 45 | inversePrimary = md_theme_light_inversePrimary, 46 | surfaceTint = md_theme_light_surfaceTint, 47 | ) 48 | 49 | private val DarkColorScheme = darkColorScheme( 50 | primary = md_theme_dark_primary, 51 | onPrimary = md_theme_dark_onPrimary, 52 | primaryContainer = md_theme_dark_primaryContainer, 53 | onPrimaryContainer = md_theme_dark_onPrimaryContainer, 54 | secondary = md_theme_dark_secondary, 55 | onSecondary = md_theme_dark_onSecondary, 56 | secondaryContainer = md_theme_dark_secondaryContainer, 57 | onSecondaryContainer = md_theme_dark_onSecondaryContainer, 58 | tertiary = md_theme_dark_tertiary, 59 | onTertiary = md_theme_dark_onTertiary, 60 | tertiaryContainer = md_theme_dark_tertiaryContainer, 61 | onTertiaryContainer = md_theme_dark_onTertiaryContainer, 62 | error = md_theme_dark_error, 63 | errorContainer = md_theme_dark_errorContainer, 64 | onError = md_theme_dark_onError, 65 | onErrorContainer = md_theme_dark_onErrorContainer, 66 | background = md_theme_dark_background, 67 | onBackground = md_theme_dark_onBackground, 68 | surface = md_theme_dark_surface, 69 | onSurface = md_theme_dark_onSurface, 70 | surfaceVariant = md_theme_dark_surfaceVariant, 71 | onSurfaceVariant = md_theme_dark_onSurfaceVariant, 72 | outline = md_theme_dark_outline, 73 | inverseOnSurface = md_theme_dark_inverseOnSurface, 74 | inverseSurface = md_theme_dark_inverseSurface, 75 | inversePrimary = md_theme_dark_inversePrimary, 76 | surfaceTint = md_theme_dark_surfaceTint, 77 | ) 78 | 79 | @Composable 80 | fun MseaComposeTheme( 81 | darkTheme: Boolean = isSystemInDarkTheme(), 82 | isDynamicColor: Boolean = false, 83 | content: @Composable () -> Unit 84 | ) { 85 | // Dynamic color is available on Android 12+ 86 | val dynamicColor = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S 87 | val colorScheme = when { 88 | dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current) 89 | dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current) 90 | darkTheme -> DarkColorScheme 91 | else -> LightColorScheme 92 | } 93 | 94 | val view = LocalView.current 95 | if (!view.isInEditMode) { 96 | SideEffect { 97 | (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() 98 | @Suppress("DEPRECATION") 99 | ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme 100 | } 101 | } 102 | 103 | MaterialTheme( 104 | colorScheme = colorScheme, 105 | typography = AppTypography, 106 | content = content 107 | ) 108 | } 109 | 110 | @Composable 111 | fun themeStyleDark(): Boolean { 112 | val themeStyle = SettingInfo.instance.themeStyle 113 | return if (themeStyle == 0) { 114 | isSystemInDarkTheme() 115 | } else themeStyle != 1 116 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.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 | //Replace with your font locations 10 | val Roboto = FontFamily.Default 11 | 12 | 13 | val AppTypography = Typography( 14 | labelLarge = TextStyle( 15 | fontFamily = Roboto, 16 | fontWeight = FontWeight.Medium, 17 | letterSpacing = 0.sp, 18 | lineHeight = 20.sp, 19 | fontSize = 14.sp 20 | ), 21 | labelMedium = TextStyle( 22 | fontFamily = Roboto, 23 | fontWeight = FontWeight.Medium, 24 | letterSpacing = 0.10000000149011612.sp, 25 | lineHeight = 16.sp, 26 | fontSize = 12.sp 27 | ), 28 | labelSmall = TextStyle( 29 | fontFamily = Roboto, 30 | fontWeight = FontWeight.Medium, 31 | letterSpacing = 0.10000000149011612.sp, 32 | lineHeight = 16.sp, 33 | fontSize = 11.sp 34 | ), 35 | bodyLarge = TextStyle( 36 | fontFamily =Roboto, 37 | fontWeight = FontWeight.W400, 38 | letterSpacing = 0.sp, 39 | lineHeight = 24.sp, 40 | fontSize = 16.sp 41 | ), 42 | bodyMedium = TextStyle( 43 | fontFamily = Roboto, 44 | fontWeight = FontWeight.W400, 45 | letterSpacing = 0.sp, 46 | lineHeight = 20.sp, 47 | fontSize = 14.sp 48 | ), 49 | bodySmall = TextStyle( 50 | fontFamily = Roboto, 51 | fontWeight = FontWeight.W400, 52 | letterSpacing = 0.10000000149011612.sp, 53 | lineHeight = 16.sp, 54 | fontSize = 12.sp 55 | ), 56 | headlineLarge = TextStyle( 57 | fontFamily = Roboto, 58 | fontWeight = FontWeight.W400, 59 | letterSpacing = 0.sp, 60 | lineHeight = 40.sp, 61 | fontSize = 32.sp 62 | ), 63 | headlineMedium = TextStyle( 64 | fontFamily = Roboto, 65 | fontWeight = FontWeight.W400, 66 | letterSpacing = 0.sp, 67 | lineHeight = 36.sp, 68 | fontSize = 28.sp 69 | ), 70 | headlineSmall = TextStyle( 71 | fontFamily =Roboto, 72 | fontWeight = FontWeight.W400, 73 | letterSpacing = 0.sp, 74 | lineHeight = 32.sp, 75 | fontSize = 24.sp 76 | ), 77 | displayLarge = TextStyle( 78 | fontFamily = Roboto, 79 | fontWeight = FontWeight.W400, 80 | letterSpacing = 0.sp, 81 | lineHeight = 64.sp, 82 | fontSize = 57.sp 83 | ), 84 | displayMedium = TextStyle( 85 | fontFamily = Roboto, 86 | fontWeight = FontWeight.W400, 87 | letterSpacing = 0.sp, 88 | lineHeight = 52.sp, 89 | fontSize = 45.sp 90 | ), 91 | displaySmall = TextStyle( 92 | fontFamily = Roboto, 93 | fontWeight = FontWeight.W400, 94 | letterSpacing = 0.sp, 95 | lineHeight = 44.sp, 96 | fontSize = 36.sp 97 | ), 98 | titleLarge = TextStyle( 99 | fontFamily = Roboto, 100 | fontWeight = FontWeight.W400, 101 | letterSpacing = 0.sp, 102 | lineHeight = 28.sp, 103 | fontSize = 22.sp 104 | ), 105 | titleMedium = TextStyle( 106 | fontFamily = Roboto, 107 | fontWeight = FontWeight.Medium, 108 | letterSpacing = 0.sp, 109 | lineHeight = 24.sp, 110 | fontSize = 16.sp 111 | ), 112 | titleSmall = TextStyle( 113 | fontFamily = Roboto, 114 | fontWeight = FontWeight.Medium, 115 | letterSpacing = 0.sp, 116 | lineHeight = 20.sp, 117 | fontSize = 14.sp 118 | ), 119 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/widget/CommonWidget.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.widget 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.IntrinsicSize 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.layout.width 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.LocalTextStyle 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Surface 17 | import androidx.compose.material3.Text 18 | import androidx.compose.material3.TextButton 19 | import androidx.compose.runtime.* 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.graphics.Color 23 | import androidx.compose.ui.res.painterResource 24 | import androidx.compose.ui.text.style.TextOverflow 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.window.Dialog 27 | import androidx.compose.ui.window.DialogProperties 28 | import com.eternaljust.msea.R 29 | 30 | @Composable 31 | fun ListArrowForward() { 32 | Icon( 33 | modifier = Modifier.size(20.dp), 34 | painter = painterResource(id = R.drawable.ic_baseline_arrow_forward_ios_24), 35 | tint = Color.LightGray, 36 | contentDescription = null 37 | ) 38 | } 39 | 40 | @Composable 41 | fun AutosizeText( 42 | text: String, 43 | multiplierConstant: Float = 0.99f 44 | ) { 45 | var multiplier by remember { mutableStateOf(1f) } 46 | 47 | Text( 48 | text = text, 49 | maxLines = 1, 50 | overflow = TextOverflow.Visible, 51 | style = LocalTextStyle.current.copy( 52 | fontSize = LocalTextStyle.current.fontSize * multiplier 53 | ), 54 | onTextLayout = { 55 | if (it.hasVisualOverflow) { 56 | multiplier *= multiplierConstant 57 | } 58 | } 59 | ) 60 | } 61 | 62 | @Composable 63 | fun TimePickerDialog( 64 | title: String = "Select Time", 65 | onCancel: () -> Unit, 66 | onConfirm: () -> Unit, 67 | toggle: @Composable () -> Unit = {}, 68 | content: @Composable () -> Unit, 69 | ) { 70 | Dialog( 71 | onDismissRequest = onCancel, 72 | properties = DialogProperties( 73 | usePlatformDefaultWidth = false 74 | ), 75 | ) { 76 | Surface( 77 | shape = MaterialTheme.shapes.extraLarge, 78 | tonalElevation = 6.dp, 79 | modifier = Modifier 80 | .width(IntrinsicSize.Min) 81 | .height(IntrinsicSize.Min) 82 | .background( 83 | shape = MaterialTheme.shapes.extraLarge, 84 | color = MaterialTheme.colorScheme.surface 85 | ), 86 | ) { 87 | toggle() 88 | 89 | Column( 90 | modifier = Modifier.padding(24.dp), 91 | horizontalAlignment = Alignment.CenterHorizontally 92 | ) { 93 | Text( 94 | modifier = Modifier 95 | .fillMaxWidth() 96 | .padding(bottom = 20.dp), 97 | text = title, 98 | style = MaterialTheme.typography.labelMedium 99 | ) 100 | 101 | content() 102 | 103 | Row( 104 | modifier = Modifier 105 | .height(40.dp) 106 | .fillMaxWidth() 107 | ) { 108 | Spacer(modifier = Modifier.weight(1f)) 109 | 110 | TextButton( 111 | onClick = onCancel 112 | ) { 113 | Text("取消") 114 | } 115 | 116 | TextButton( 117 | onClick = onConfirm 118 | ) { 119 | Text("确认") 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/ui/widget/TopAppBar.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.ui.widget 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.ArrowBack 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.graphics.Color 8 | 9 | @OptIn(ExperimentalMaterial3Api::class) 10 | @Composable 11 | fun mseaTopAppBarColors(): TopAppBarColors { 12 | return TopAppBarDefaults.topAppBarColors( 13 | containerColor = MaterialTheme.colorScheme.primary, 14 | titleContentColor = Color.White, 15 | actionIconContentColor = Color.White, 16 | navigationIconContentColor = Color.White 17 | ) 18 | } 19 | 20 | @OptIn(ExperimentalMaterial3Api::class) 21 | @Composable 22 | fun NormalTopAppBar( 23 | title: String = "", 24 | onClick: () -> Unit 25 | ) { 26 | TopAppBar( 27 | title = { Text(title) }, 28 | navigationIcon = { 29 | IconButton( 30 | onClick = onClick 31 | ) { 32 | Icon( 33 | imageVector = Icons.Default.ArrowBack, 34 | contentDescription = "返回" 35 | ) 36 | } 37 | }, 38 | colors = mseaTopAppBarColors() 39 | ) 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.utils 2 | 3 | object Constants { 4 | const val umAppkey = "637ddbe888ccdf4b7e6cbd1e" 5 | const val termsOfService = """ 6 | Msea 作为一款虫部落开源第三方 App,App 只提供登录、签到、浏览查看等功能,不提供注册相关的服务,具体使用条款请查看以下虫部落免责声明: 7 | 8 | 虫部落提醒您:在使用虫部落前,请您务必仔细阅读并透彻理解本声明。您可以选择不使用虫部落,但如果您使用虫部落,您的使用行为将被视为对本声明全部内容的认可。 9 | 10 | 本站搜索聚合工具(快搜、学术搜索、搜书等)所收录等第三方搜索引擎的搜索算法、数据和搜索结果均属其个人或组织行为,不代表本站立场。 11 | 12 | 鉴于本站搜索聚合工具未使用自建索引/数据存储/分析模式,无法确定您输入的条件进行是否合法,也无法实时监控第三方网站的搜索结果的合法性,所以本站对搜索聚合工具页面检索/分析出的结果不承担责任。如果因以本站的检索/分析结果作为任何商业行为或者学术研究的依据而产生不良后果,虫部落不承担任何法律责任。 13 | 14 | 任何通过使用本站搜索聚合工具中的第三方搜索引擎而搜索链接到的其它第三方网页均系他人制作或提供,您可能从该第三方网页上获得资讯及享用服务,虫部落对其合法性概不负责,亦不承担任何法律责任。 15 | 16 | 虫部落注册用户在社区发布的任何软件、插件和脚本等程序,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断,虫部落不承担任何法律责任。 17 | 18 | 虫部落所有资源文件,禁止任何公众号、自媒体进行任何形式的转载、发布。 19 | 20 | 虫部落对任何个人发布的软件、插件和脚本等程序问题概不负责,包括但不限于由任软件、插件和脚本等程序错误导致的任何损失或损害。 21 | 22 | 请勿将虫部落的任何内容用于商业或非法目的,否则后果自负。如果任何单位或个人认为虫部落的相关内容可能涉嫌侵犯其权利,则应及时通知管理员并提供身份证明,所有权证明,管理员将在收到认证文件后删除相关脚本。 23 | 24 | 再次重申:您访问、浏览、使用或者复制了虫部落的任何内容,则视为已接受此声明,请仔细阅读! 25 | """ 26 | const val privacyFileUrl = "file:///android_asset/html/privacy.html" 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/utils/DataStoreUtil.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.utils 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.* 6 | import androidx.datastore.preferences.preferencesDataStore 7 | import kotlinx.coroutines.flow.first 8 | import kotlinx.coroutines.runBlocking 9 | 10 | object DataStoreUtil { 11 | private val Context.dataStore: DataStore by preferencesDataStore( 12 | name = "MseaDataStore" 13 | ) 14 | private lateinit var dataStore: DataStore 15 | 16 | fun init(context: Context) { 17 | dataStore = context.dataStore 18 | } 19 | 20 | fun getData( 21 | key: String, 22 | default: U 23 | ): U { 24 | val res = when (default) { 25 | is String -> getStringData(key, default) 26 | is Int -> getIntData(key, default) 27 | is Boolean -> getBooleanData(key, default) 28 | else -> throw IllegalArgumentException("This type can't be readied into DataStore") 29 | } 30 | @Suppress("UNCHECKED_CAST") 31 | return res as U 32 | } 33 | 34 | private suspend fun setData( 35 | key: String, 36 | value: U 37 | ) { 38 | when (value) { 39 | is String -> setStringData(key, value) 40 | is Int -> setIntData(key, value) 41 | is Boolean -> setBooleanData(key, value) 42 | else -> throw IllegalArgumentException("This type can't be saved into DataStore") 43 | } 44 | } 45 | 46 | fun syncSetData( 47 | key: String, 48 | value: U 49 | ) { 50 | runBlocking { setData(key, value) } 51 | } 52 | 53 | private suspend fun removeData( 54 | key: String, 55 | value: U 56 | ) { 57 | when (value) { 58 | is String -> removeStringData(key) 59 | is Int -> removeIntData(key) 60 | is Boolean -> removeBooleanData(key) 61 | else -> throw IllegalArgumentException("This type can't be removed into DataStore") 62 | } 63 | } 64 | 65 | fun syncRemoveData( 66 | key: String, 67 | value: U 68 | ) { 69 | runBlocking { removeData(key, value) } 70 | } 71 | 72 | private fun getStringData( 73 | key: String, 74 | default: String = "" 75 | ): String { 76 | var value = default 77 | runBlocking { 78 | dataStore.data.first { 79 | value = it[stringPreferencesKey(key)] ?: default 80 | true 81 | } 82 | } 83 | return value 84 | } 85 | 86 | private fun getIntData( 87 | key: String, 88 | default: Int = 0 89 | ): Int { 90 | var value = default 91 | runBlocking { 92 | dataStore.data.first { 93 | value = it[intPreferencesKey(key)] ?: default 94 | true 95 | } 96 | } 97 | return value 98 | } 99 | 100 | private fun getBooleanData( 101 | key: String, 102 | default: Boolean = false 103 | ): Boolean { 104 | var value = default 105 | runBlocking { 106 | dataStore.data.first { 107 | value = it[booleanPreferencesKey(key)] ?: default 108 | true 109 | } 110 | } 111 | return value 112 | } 113 | 114 | private suspend fun setStringData( 115 | key: String, 116 | value: String 117 | ) { 118 | dataStore.edit { 119 | it[stringPreferencesKey(key)] = value 120 | } 121 | } 122 | 123 | private suspend fun setIntData( 124 | key: String, 125 | value: Int 126 | ) { 127 | dataStore.edit { 128 | it[intPreferencesKey(key)] = value 129 | } 130 | } 131 | 132 | private suspend fun setBooleanData( 133 | key: String, 134 | value: Boolean 135 | ) { 136 | dataStore.edit { 137 | it[booleanPreferencesKey(key)] = value 138 | } 139 | } 140 | 141 | private suspend fun removeStringData(key: String) { 142 | dataStore.edit { 143 | it.remove(stringPreferencesKey(key)) 144 | } 145 | } 146 | 147 | private suspend fun removeIntData(key: String) { 148 | dataStore.edit { 149 | it.remove(intPreferencesKey(key)) 150 | } 151 | } 152 | 153 | private suspend fun removeBooleanData(key: String) { 154 | dataStore.edit { 155 | it.remove(booleanPreferencesKey(key)) 156 | } 157 | } 158 | 159 | suspend fun clear() { 160 | dataStore.edit { 161 | it.clear() 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/utils/JsonUtil.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.utils 2 | 3 | import android.os.Parcelable 4 | import com.google.gson.Gson 5 | import com.google.gson.JsonSyntaxException 6 | 7 | fun Parcelable.toJson(): String { 8 | return Gson().toJson(this) 9 | } 10 | 11 | inline fun String.fromJson(): T? { 12 | return try { 13 | Gson().fromJson(this, T::class.java) 14 | } catch (e: JsonSyntaxException) { 15 | println("Gson failed: ${e.message}") 16 | null 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/utils/Paging.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.utils 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import androidx.paging.* 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | fun ViewModel.configPager( 9 | config: PagingConfig, 10 | callAction: suspend (page: Int) -> List 11 | ): Flow> { 12 | return pager(config) { 13 | val nextPage = it.key ?: 1 14 | val response = try { 15 | HttpResult.Success(callAction.invoke(nextPage)) 16 | } catch (e: Exception) { 17 | HttpResult.Error(e) 18 | } 19 | when (response) { 20 | is HttpResult.Success -> { 21 | val data = response.result 22 | PagingSource.LoadResult.Page( 23 | data = data, 24 | prevKey = if (nextPage == 1) null else nextPage - 1, 25 | nextKey = if (data.isEmpty()) null else nextPage + 1 26 | ) 27 | } 28 | is HttpResult.Error -> { 29 | PagingSource.LoadResult.Error(response.exception) 30 | } 31 | } 32 | } 33 | } 34 | 35 | fun ViewModel.pager( 36 | config: PagingConfig, 37 | loadData: suspend (PagingSource.LoadParams) -> PagingSource.LoadResult 38 | ): Flow> { 39 | val baseConfig = PagingConfig( 40 | config.pageSize, 41 | initialLoadSize = config.initialLoadSize, 42 | prefetchDistance = config.prefetchDistance, 43 | maxSize = config.maxSize, 44 | enablePlaceholders = config.enablePlaceholders 45 | ) 46 | return Pager( 47 | config = baseConfig 48 | ) { 49 | object : PagingSource() { 50 | override suspend fun load(params: LoadParams): LoadResult { 51 | return loadData.invoke(params) 52 | } 53 | 54 | override fun getRefreshKey(state: PagingState): K? = null 55 | } 56 | }.flow.cachedIn(viewModelScope) 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/utils/RouteUtil.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.utils 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.net.Uri 8 | import android.os.Build 9 | import com.eternaljust.msea.BuildConfig 10 | 11 | object RouteName { 12 | const val HOME = "home" 13 | const val NOTICE = "notice" 14 | const val NODE = "node" 15 | const val NODE_DETAIL = "node_detail" 16 | const val TOPIC_DETAIL = "topic_detail" 17 | const val PROFILE_TOPIC = "profile_topic" 18 | const val PROFILE_FRIEND = "profile_friend" 19 | const val PROFILE_FAVORITE = "profile_favorite" 20 | const val PROFILE_CREDIT = "profile_credit" 21 | const val PROFILE_GROUP= "profile_group" 22 | const val PROFILE_DETAIL = "profile_detail" 23 | const val PROFILE_DETAIL_USERNAME = "profile_detail_username" 24 | const val SETTING = "setting" 25 | const val ABOUT = "about" 26 | const val LOGIN = "login" 27 | const val LOGOUT= "logout" 28 | const val SIGN = "sign" 29 | const val TAG = "tag" 30 | const val TAG_LIST = "tag_list" 31 | const val NODE_LIST = "node_list" 32 | const val TERMS_OF_SERVICE = "terms_of_service" 33 | const val PRIVACY_POLICY = "privacy_policy" 34 | const val WEBVIEW = "webview" 35 | const val LICENSE = "license" 36 | const val SOURCE_CODE = "source_code" 37 | const val SDK_LIST = "sdk_list" 38 | const val UPDATE_VERSION = "update_version" 39 | const val SEARCH = "search" 40 | } 41 | 42 | fun isAppInstalled( 43 | packageName: String, 44 | context: Context 45 | ) : Boolean { 46 | val pm = context.packageManager 47 | // 系统应用uid从1000开始,用户应用uid从10000(FIRST_APPLICATION_UID)开始,直接合并查询 48 | for (i in 10000..11000) { 49 | try { 50 | val apps = pm.getPackagesForUid(i) 51 | if (apps != null) { 52 | for (app in apps) { 53 | @Suppress("DEPRECATION") 54 | val info = pm.getPackageInfo(app!!, 0) 55 | if (info != null && info.packageName == packageName) { 56 | return true 57 | } 58 | } 59 | } 60 | } catch (e: Exception) { 61 | e.printStackTrace() 62 | } 63 | } 64 | return false 65 | } 66 | 67 | fun openSystemBrowser( 68 | url: String, 69 | context: Context 70 | ) { 71 | val uri = Uri.parse(url) 72 | val intent = Intent(Intent.ACTION_VIEW, uri) 73 | context.startActivity(intent) 74 | } 75 | 76 | fun openApp( 77 | url: String, 78 | context: Context 79 | ) { 80 | val intent: Intent = Intent().apply { 81 | action = Intent.ACTION_VIEW 82 | data = Uri.parse(url) 83 | } 84 | context.startActivity(intent) 85 | } 86 | 87 | fun openSystemShare( 88 | text: String, 89 | title: String = "", 90 | context: Context 91 | ) { 92 | val sendIntent: Intent = Intent().apply { 93 | action = Intent.ACTION_SEND 94 | putExtra(Intent.EXTRA_TEXT, text) 95 | putExtra(Intent.EXTRA_TITLE, title) 96 | type = "text/plain" 97 | } 98 | val shareIntent = Intent.createChooser(sendIntent, null) 99 | context.startActivity(shareIntent) 100 | } 101 | 102 | fun textCopyThenPost( 103 | textCopied: String, 104 | context: Context 105 | ) { 106 | val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 107 | // When setting the clip board text. 108 | clipboardManager.setPrimaryClip(ClipData.newPlainText("", textCopied)) 109 | // Only show a toast for Android 12 and lower. 110 | } 111 | 112 | fun sendEmail(context: Context) { 113 | val toRecipient = Uri.parse("mailto:eternal.just@gmail.com") 114 | val title = "Msea ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) 问题反馈" 115 | val content = """ 116 | 设备来自:${Build.BRAND} ${Build.MODEL} / Android ${Build.VERSION.RELEASE} 117 | 118 | 1.描述遇到的问题,方便的话添加错误页面截图。 119 | 120 | 121 | 2.能否复现问题?可以的话给出具体的步骤。 122 | 123 | 124 | 3.非 bug 反馈,有其他的想法。 125 | 126 | """ 127 | 128 | val intent = Intent(Intent.ACTION_SENDTO) 129 | intent.data = toRecipient 130 | intent.putExtra(Intent.EXTRA_SUBJECT, title) 131 | intent.putExtra(Intent.EXTRA_TEXT, content) 132 | context.startActivity(intent) 133 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/utils/SettingCacheInfo.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.utils 2 | 3 | class SettingInfo { 4 | companion object { 5 | val instance by lazy { SettingInfo() } 6 | } 7 | 8 | var colorScheme: Boolean 9 | get() = DataStoreUtil.getData(SettingInfoKey.COLOR_SCHEME, false) 10 | set(value) = DataStoreUtil.syncSetData(SettingInfoKey.COLOR_SCHEME, value) 11 | 12 | var themeStyle: Int 13 | get() = DataStoreUtil.getData(SettingInfoKey.THEME_STYLE, 0) 14 | set(value) = DataStoreUtil.syncSetData(SettingInfoKey.THEME_STYLE, value) 15 | 16 | var agreePrivacyPolicy: Boolean 17 | get() = DataStoreUtil.getData(SettingInfoKey.AGREE_PRIVACY_POLICY, false) 18 | set(value) = DataStoreUtil.syncSetData(SettingInfoKey.AGREE_PRIVACY_POLICY, value) 19 | } 20 | 21 | object SettingInfoKey { 22 | const val COLOR_SCHEME = "colorSchemeKey" 23 | const val THEME_STYLE = "themeStyleKey" 24 | const val DAY_SIGN_SWITCH = "daysignSwitchKey" 25 | const val DAY_SIGN_HOUR = "daysignHourKey" 26 | const val DAY_SIGN_MINUTE = "daysignMinuteKey" 27 | const val AGREE_PRIVACY_POLICY = "agreePrivacyPolicyKey" 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/utils/StatisticsTool.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.utils 2 | 3 | import android.content.Context 4 | import com.umeng.analytics.MobclickAgent 5 | import com.umeng.pagesdk.PageManger.getApplicationContext 6 | 7 | // 友盟埋点统计自定义事件 8 | class StatisticsTool { 9 | companion object { 10 | val instance by lazy { StatisticsTool() } 11 | } 12 | 13 | fun eventObject( 14 | event: String, 15 | keyAndValue: Map 16 | ) { 17 | MobclickAgent.onEventObject(getApplicationContext(), event, keyAndValue) 18 | } 19 | 20 | fun eventObject( 21 | context: Context, 22 | resId: Int, 23 | keyAndValue: Map 24 | ) { 25 | val event = context.getString(resId) 26 | val params: MutableMap = mutableMapOf() 27 | keyAndValue.forEach { (k, v) -> 28 | params[context.getString(k)] = v 29 | } 30 | println("eventObject---event=${event}, params=$params") 31 | 32 | MobclickAgent.onEventObject(context, event, params) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/eternaljust/msea/utils/UserCacheInfo.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea.utils 2 | 3 | class UserInfo { 4 | companion object { 5 | val instance by lazy { UserInfo() } 6 | } 7 | 8 | var auth: String 9 | get() = DataStoreUtil.getData(UserInfoKey.AUTH, "") 10 | set(value) = DataStoreUtil.syncSetData(UserInfoKey.AUTH, value) 11 | 12 | var salt: String 13 | get() = DataStoreUtil.getData(UserInfoKey.SALT, "") 14 | set(value) = DataStoreUtil.syncSetData(UserInfoKey.SALT, value) 15 | 16 | var formhash: String 17 | get() = DataStoreUtil.getData(UserInfoKey.FORMHASH, "") 18 | set(value) = DataStoreUtil.syncSetData(UserInfoKey.FORMHASH, value) 19 | 20 | var uid: String 21 | get() = DataStoreUtil.getData(UserInfoKey.UID, "") 22 | set(value) = DataStoreUtil.syncSetData(UserInfoKey.UID, value) 23 | 24 | var name: String 25 | get() = DataStoreUtil.getData(UserInfoKey.NAME, "") 26 | set(value) = DataStoreUtil.syncSetData(UserInfoKey.NAME, value) 27 | 28 | var level: String 29 | get() = DataStoreUtil.getData(UserInfoKey.LEVEL, "") 30 | set(value) = DataStoreUtil.syncSetData(UserInfoKey.LEVEL, value) 31 | 32 | var avatar: String 33 | get() = DataStoreUtil.getData(UserInfoKey.AVATAR, "") 34 | set(value) = DataStoreUtil.syncSetData(UserInfoKey.AVATAR, value) 35 | 36 | var friend: String 37 | get() = DataStoreUtil.getData(UserInfoKey.FRIEND, "") 38 | set(value) = DataStoreUtil.syncSetData(UserInfoKey.FRIEND, value) 39 | 40 | var reply: String 41 | get() = DataStoreUtil.getData(UserInfoKey.REPLY, "") 42 | set(value) = DataStoreUtil.syncSetData(UserInfoKey.REPLY, value) 43 | 44 | var topic: String 45 | get() = DataStoreUtil.getData(UserInfoKey.TOPIC, "") 46 | set(value) = DataStoreUtil.syncSetData(UserInfoKey.TOPIC, value) 47 | 48 | var integral: String 49 | get() = DataStoreUtil.getData(UserInfoKey.INTEGRAL, "") 50 | set(value) = DataStoreUtil.syncSetData(UserInfoKey.INTEGRAL, value) 51 | 52 | var bits: String 53 | get() = DataStoreUtil.getData(UserInfoKey.BITS, "") 54 | set(value) = DataStoreUtil.syncSetData(UserInfoKey.BITS, value) 55 | 56 | var violation: String 57 | get() = DataStoreUtil.getData(UserInfoKey.VIOLATION, "") 58 | set(value) = DataStoreUtil.syncSetData(UserInfoKey.VIOLATION, value) 59 | 60 | fun clear() { 61 | DataStoreUtil.syncRemoveData(UserInfoKey.AUTH, auth) 62 | DataStoreUtil.syncRemoveData(UserInfoKey.SALT, salt) 63 | DataStoreUtil.syncRemoveData(UserInfoKey.FORMHASH, formhash) 64 | DataStoreUtil.syncRemoveData(UserInfoKey.UID, uid) 65 | DataStoreUtil.syncRemoveData(UserInfoKey.NAME, name) 66 | DataStoreUtil.syncRemoveData(UserInfoKey.LEVEL, level) 67 | DataStoreUtil.syncRemoveData(UserInfoKey.AVATAR, avatar) 68 | DataStoreUtil.syncRemoveData(UserInfoKey.FRIEND, friend) 69 | DataStoreUtil.syncRemoveData(UserInfoKey.REPLY, reply) 70 | DataStoreUtil.syncRemoveData(UserInfoKey.TOPIC, topic) 71 | DataStoreUtil.syncRemoveData(UserInfoKey.INTEGRAL, integral) 72 | DataStoreUtil.syncRemoveData(UserInfoKey.BITS, bits) 73 | DataStoreUtil.syncRemoveData(UserInfoKey.VIOLATION, violation) 74 | } 75 | } 76 | 77 | object UserInfoKey { 78 | const val AUTH = "authKey" 79 | const val SALT = "saltKey" 80 | const val FORMHASH = "formhashKey" 81 | const val UID = "uidKey" 82 | const val NAME = "nameKey" 83 | const val LEVEL = "levelKey" 84 | const val AVATAR = "avatarKey" 85 | const val FRIEND = "friendKey" 86 | const val REPLY = "replyKey" 87 | const val TOPIC = "topicKey" 88 | const val INTEGRAL = "integralKey" 89 | const val BITS = "bitsKey" 90 | const val VIOLATION = "violationKey" 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_admin_panel_settings_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_android_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_arrow_downward_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_arrow_drop_down_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_arrow_drop_up_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_arrow_forward_ios_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_arrow_upward_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_business_center_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_calendar_month_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_cloud_download_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_computer_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_contacts_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_currency_exchange_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_dark_mode_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_desktop_mac_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_elderly_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_energy_savings_leaf_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_event_available_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_feed_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_feedback_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_g_translate_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_grid_view_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_group_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_help_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_image_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_key_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_keyboard_double_arrow_right_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_laptop_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_light_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_lightbulb_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_link_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_local_fire_department_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_logout_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_man_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_paid_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_pin_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_privacy_tip_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_public_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_restaurant_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_school_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_settings_brightness_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_sms_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_sports_soccer_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_swap_horiz_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_tag_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_topic_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_view_list_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_visibility_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_visibility_off_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_waving_hand_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_wb_sunny_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_web_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_woman_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_workspace_premium_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_zoom_in_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 17 | 23 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eternaljust/msea-compose/ccd1c7c1f6b77cbd7a77aaf0812962ef2ec8812e/app/src/main/res/drawable/icon.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #FFFFFFFF 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Msea 3 | 虫部落 4 | 通知 5 | 节点 6 | sign 7 | 1 8 | 9 | 10 | event_page_home 11 | 12 | key_category 13 | 14 | key_tab 15 | 16 | key_list_page 17 | 18 | event_page_tab 19 | key_name_home 20 | key_name_notice 21 | key_name_sign 22 | key_name_search 23 | key_name_friend 24 | key_name_credit 25 | key_name_profile 26 | 27 | event_list_drawer 28 | 29 | key_item 30 | 31 | key_setting 32 | 33 | key_setting_dark 34 | 35 | key_setting_color 36 | 37 | key_setting_sign 38 | 39 | key_setting_time 40 | 41 | key_about 42 | 43 | key_about_update 44 | 45 | key_logout 46 | 47 | event_topic_detail 48 | 49 | key_source 50 | 51 | key_action 52 | 53 | key_link 54 | 55 | key_node 56 | 57 | key_tag 58 | 59 | key_page_click 60 | 61 | key_comment 62 | 63 | key_reply 64 | 65 | event_page_profile 66 | 67 | event_page_login 68 | 69 | key_location 70 | 71 | key_register 72 | 73 | key_login_type 74 | 75 | key_login_question 76 | 77 | key_login_failed 78 | 79 | event_page_sign 80 | 81 | key_login 82 | 83 | key_sign 84 | 85 | key_sign_result 86 | 87 | event_page_search 88 | 89 | key_search 90 | 91 | event_page_notice 92 | 93 | event_page_node 94 | 95 | key_forum 96 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/cloud_config_parms.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | config_version 5 | {"versionCode":17,"versionName":"1.0.8","versionContent":"1.顶部导航栏按钮添加文本:菜单、搜索、签到与标签","updateTime":"2023.04.13"} 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/test/java/com/eternaljust/msea/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.eternaljust.msea 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | compose_version = '1.7.8' 4 | accompanist_version = '0.36.0' 5 | } 6 | repositories { 7 | mavenCentral() 8 | } 9 | }// Top-level build file where you can add configuration options common to all sub-projects/modules. 10 | plugins { 11 | id 'com.android.application' version '8.9.1' apply false 12 | id 'com.android.library' version '8.9.1' apply false 13 | id 'org.jetbrains.kotlin.android' version '2.1.20' apply false 14 | id 'org.jetbrains.kotlin.plugin.compose' version '2.1.20' apply false 15 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | org.gradle.unsafe.configuration-cache=true 25 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eternaljust/msea-compose/ccd1c7c1f6b77cbd7a77aaf0812962ef2ec8812e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jan 16 16:10:51 CST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "msea-compose" 16 | include ':app' 17 | --------------------------------------------------------------------------------