├── composeApp └── src │ ├── commonMain │ ├── composeResources │ │ ├── files │ │ │ ├── strings_en.properties │ │ │ └── strings_zh.properties │ │ └── drawable │ │ │ ├── ic_launcher.png │ │ │ ├── tips_warning.xml │ │ │ ├── tips_finish.xml │ │ │ └── tips_error.xml │ └── kotlin │ │ └── com │ │ └── qust │ │ └── helper │ │ ├── ui │ │ ├── drawables │ │ │ ├── Drawables.kt │ │ │ ├── Electric.kt │ │ │ ├── IconLogin.kt │ │ │ ├── Login.kt │ │ │ ├── Water.kt │ │ │ ├── Visibility.kt │ │ │ ├── School.kt │ │ │ ├── Notification.kt │ │ │ ├── Article.kt │ │ │ ├── InsertInvitation.kt │ │ │ ├── Brightness.kt │ │ │ ├── VisibilityOff.kt │ │ │ └── GridView.kt │ │ ├── widget │ │ │ ├── Modifier.kt │ │ │ ├── lesson │ │ │ │ └── lessonEdit │ │ │ │ │ └── LessonEditUIState.kt │ │ │ ├── toast │ │ │ │ └── ToastUIState.kt │ │ │ ├── components │ │ │ │ ├── Texts.kt │ │ │ │ └── Loading.kt │ │ │ ├── dialog │ │ │ │ └── BottomDialog.kt │ │ │ ├── layout │ │ │ │ └── AppLayout.kt │ │ │ └── form │ │ │ │ └── Login.kt │ │ ├── page │ │ │ ├── PageController.kt │ │ │ ├── EmptyPage.kt │ │ │ ├── BasePage.kt │ │ │ ├── lesson │ │ │ │ └── LessonTablePage.kt │ │ │ ├── eas │ │ │ │ ├── EasCompose.kt │ │ │ │ └── QueryNoticePage.kt │ │ │ └── account │ │ │ │ ├── EasLoginPage.kt │ │ │ │ └── IpassLoginPage.kt │ │ └── theme │ │ │ └── Theme.kt │ │ ├── Platform.kt │ │ ├── entity │ │ ├── vo │ │ │ └── EasLogin.kt │ │ ├── eas │ │ │ ├── Notice.kt │ │ │ ├── Exam.kt │ │ │ └── Academic.kt │ │ └── lesson │ │ │ └── Lesson.kt │ │ ├── utils │ │ ├── EasUtils.kt │ │ ├── Logger.kt │ │ ├── CodeUtils.kt │ │ ├── JsonUtils.kt │ │ ├── LessonUtils.kt │ │ ├── SettingUtils.kt │ │ └── DateUtils.kt │ │ ├── viewmodel │ │ ├── MyViewModel.kt │ │ ├── HomeViewModel.kt │ │ ├── RequestViewModel.kt │ │ ├── eas │ │ │ ├── BaseEasViewModel.kt │ │ │ ├── QueryNoticeViewModel.kt │ │ │ ├── QueryExamViewModel.kt │ │ │ └── QueryLessonViewModel.kt │ │ ├── extend │ │ │ ├── LoadingAble.kt │ │ │ ├── RequestAble.kt │ │ │ └── ToastAble.kt │ │ ├── BaseViewModel.kt │ │ ├── account │ │ │ ├── AccountManagerViewModel.kt │ │ │ ├── IpassLoginViewModel.kt │ │ │ └── EasLoginViewModel.kt │ │ ├── third │ │ │ └── DrinkViewModel.kt │ │ └── app │ │ │ └── SettingViewModel.kt │ │ ├── model │ │ ├── network │ │ │ ├── Exceptions.kt │ │ │ └── Network.kt │ │ ├── database │ │ │ ├── ExamStorage.kt │ │ │ ├── MarkStorage.kt │ │ │ ├── LessonTableStorage.kt │ │ │ └── AcademicStorage.kt │ │ ├── SettingModel.kt │ │ ├── eas │ │ │ ├── ExamModel.kt │ │ │ ├── NoticeModel.kt │ │ │ └── MarkModel.kt │ │ ├── lessonTable │ │ │ └── LessonTableModel.kt │ │ └── account │ │ │ └── DrinkAccount.kt │ │ ├── platform │ │ └── utils │ │ │ └── SettingUtilsImpl.kt │ │ └── data │ │ ├── i18n │ │ └── Strings.kt │ │ ├── Pages.kt │ │ ├── Keys.kt │ │ └── QustApi.kt │ ├── androidMain │ ├── res │ │ ├── xml │ │ │ ├── provider_apk.xml │ │ │ ├── network_security_config.xml │ │ │ ├── backup_rules.xml │ │ │ ├── daily_lesson.xml │ │ │ ├── data_extraction_rules.xml │ │ │ ├── drink_code.xml │ │ │ └── term_lesson.xml │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-nodpi │ │ │ └── lesson_preview.png │ │ ├── drawable │ │ │ ├── toast_background.xml │ │ │ ├── card_background.xml │ │ │ ├── ic_electric.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_login.xml │ │ │ ├── ic_arrow_left.xml │ │ │ ├── ic_arrow_right.xml │ │ │ ├── ic_visibility.xml │ │ │ ├── ic_water.xml │ │ │ ├── ic_notification.xml │ │ │ ├── ic_school.xml │ │ │ ├── ic_insert_invitation.xml │ │ │ ├── ic_article.xml │ │ │ ├── ic_brightness.xml │ │ │ ├── ic_view_list.xml │ │ │ ├── ic_grid_view.xml │ │ │ ├── ic_visibility_off.xml │ │ │ ├── tips_warning.xml │ │ │ ├── tips_finish.xml │ │ │ └── tips_error.xml │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── themes.xml │ │ │ └── strings.xml │ │ ├── values-night │ │ │ └── colors.xml │ │ ├── layout │ │ │ ├── view_text.xml │ │ │ ├── widget_day_lesson.xml │ │ │ ├── layout_tips.xml │ │ │ └── widget_lesson.xml │ │ └── values-en │ │ │ └── strings.xml │ ├── kotlin │ │ └── com │ │ │ └── qust │ │ │ └── helper │ │ │ ├── Platform.android.kt │ │ │ ├── model │ │ │ ├── network │ │ │ │ └── Network.kt │ │ │ └── database │ │ │ │ ├── ExamStorage.kt │ │ │ │ ├── MarkStorage.kt │ │ │ │ ├── LessonTableModel.kt │ │ │ │ └── AcademicStorage.kt │ │ │ ├── ui │ │ │ ├── activity │ │ │ │ ├── MainActivity.kt │ │ │ │ └── PageActivity.kt │ │ │ ├── widget │ │ │ │ └── Modifier.kt │ │ │ ├── page │ │ │ │ └── PageController.kt │ │ │ └── Prev.kt │ │ │ ├── utils │ │ │ ├── Logger.kt │ │ │ ├── UmengUtils.kt │ │ │ ├── SettingUtils.kt │ │ │ └── NotificationUtils.kt │ │ │ ├── room │ │ │ ├── dao │ │ │ │ ├── ExamMapper.kt │ │ │ │ ├── MarkMapper.kt │ │ │ │ ├── LessonMapper.kt │ │ │ │ └── AcademicMapper.kt │ │ │ ├── entity │ │ │ │ ├── ExamDao.kt │ │ │ │ └── LessonDao.kt │ │ │ └── AppDataBase.kt │ │ │ └── App.kt │ └── AndroidManifest.xml │ ├── wasmJsMain │ ├── resources │ │ ├── styles.css │ │ └── index.html │ └── kotlin │ │ └── com │ │ └── qust │ │ └── helper │ │ ├── Platform.wasmJs.kt │ │ ├── main.kt │ │ └── utils │ │ └── SettingUtils.kt │ ├── desktopMain │ └── kotlin │ │ └── com │ │ └── qust │ │ └── helper │ │ ├── model │ │ ├── database │ │ │ ├── ExamStorage.kt │ │ │ ├── MarkStorage.kt │ │ │ ├── LessonTableModel.kt │ │ │ └── AcademicStorage.kt │ │ └── network │ │ │ └── Network.kt │ │ ├── Platform.jvm.kt │ │ ├── ui │ │ ├── widget │ │ │ └── Modifier.kt │ │ ├── page │ │ │ └── PageController.kt │ │ └── WindowPage.kt │ │ ├── main.kt │ │ ├── viewmodel │ │ └── ApplicationViewModel.kt │ │ └── utils │ │ ├── SettingUtils.kt │ │ └── Logger.kt │ └── iosMain │ └── kotlin │ └── com │ └── qust │ └── helper │ ├── MainViewController.kt │ └── Platform.ios.kt ├── iosApp ├── Configuration │ └── Config.xcconfig └── iosApp │ ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── app-icon-1024.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── iOSApp.swift │ ├── ContentView.swift │ └── Info.plist ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── captures ├── Screenshot_20240322_163017.png ├── Screenshot_20240322_163159.png └── Screenshot_20240322_163254.png ├── gradle.properties ├── .gitignore ├── .fleet └── receipt.json ├── settings.gradle.kts ├── README.md └── gradlew.bat /composeApp/src/commonMain/composeResources/files/strings_en.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/files/strings_zh.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=com.qust.helper.QustHelper 3 | APP_NAME=QustHelper -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2891954521/QUST-Assistant/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /captures/Screenshot_20240322_163017.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2891954521/QUST-Assistant/HEAD/captures/Screenshot_20240322_163017.png -------------------------------------------------------------------------------- /captures/Screenshot_20240322_163159.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2891954521/QUST-Assistant/HEAD/captures/Screenshot_20240322_163159.png -------------------------------------------------------------------------------- /captures/Screenshot_20240322_163254.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2891954521/QUST-Assistant/HEAD/captures/Screenshot_20240322_163254.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/drawables/Drawables.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.drawables 2 | 3 | object Drawables { 4 | } -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/xml/provider_apk.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/resources/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | overflow: hidden; 7 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2891954521/QUST-Assistant/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2891954521/QUST-Assistant/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2891954521/QUST-Assistant/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2891954521/QUST-Assistant/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper 2 | 3 | interface Platform { 4 | val name: String 5 | } 6 | 7 | expect fun getPlatform(): Platform -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable-nodpi/lesson_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2891954521/QUST-Assistant/HEAD/composeApp/src/androidMain/res/drawable-nodpi/lesson_preview.png -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/qust/helper/model/database/ExamStorage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.database 2 | 3 | actual fun getExamStorage(): ExamStorage = object: ExamStorage { } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/qust/helper/model/database/MarkStorage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.database 2 | 3 | actual fun getMarkStorage(): MarkStorage = object: MarkStorage { } -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2891954521/QUST-Assistant/HEAD/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2891954521/QUST-Assistant/HEAD/composeApp/src/commonMain/composeResources/drawable/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/qust/helper/model/database/LessonTableModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.database 2 | 3 | actual fun getLessonTableStorage() = object: LessonTableStorage { } -------------------------------------------------------------------------------- /iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/qust/helper/model/database/AcademicStorage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.database 2 | 3 | actual fun getAcademicStorage(): AcademicStorage = object: AcademicStorage { } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/com/qust/helper/MainViewController.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper 2 | 3 | import androidx.compose.ui.window.ComposeUIViewController 4 | 5 | fun MainViewController() = ComposeUIViewController { App() } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/entity/vo/EasLogin.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.entity.vo 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class EasPublicKey( 7 | val modulus: String 8 | ) -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/com/qust/helper/Platform.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper 2 | 3 | class WasmPlatform: Platform { 4 | override val name: String = "Web with Kotlin/Wasm" 5 | } 6 | 7 | actual fun getPlatform(): Platform = WasmPlatform() -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Kotlin 2 | kotlin.code.style=official 3 | kotlin.daemon.jvmargs=-Xmx2048M 4 | 5 | #Gradle 6 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 7 | 8 | #Android 9 | android.nonTransitiveRClass=true 10 | android.useAndroidX=true -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/toast_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/card_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/qust/helper/Platform.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper 2 | 3 | class JVMPlatform: Platform { 4 | override val name: String = "Java ${System.getProperty("java.version")}" 5 | } 6 | 7 | actual fun getPlatform(): Platform = JVMPlatform() -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/widget/Modifier.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.widget 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | 6 | 7 | @Composable 8 | expect fun Modifier.click(click: () -> Unit): Modifier -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/Platform.android.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper 2 | 3 | import android.os.Build 4 | 5 | class AndroidPlatform : Platform { 6 | override val name: String = "Android ${Build.VERSION.SDK_INT}" 7 | } 8 | 9 | actual fun getPlatform(): Platform = AndroidPlatform() -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #212121 4 | #757575 5 | #1A1C1E 6 | #FFFFFF 7 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #E2E2E2 4 | #7F7F7F 5 | #E3E2E6 6 | #212121 7 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/model/network/Network.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.network 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.HttpClientConfig 5 | import io.ktor.client.engine.okhttp.OkHttp 6 | 7 | actual fun httpClient(config: (HttpClientConfig<*>) -> Unit) = HttpClient(OkHttp, config) -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/qust/helper/model/network/Network.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.network 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.HttpClientConfig 5 | import io.ktor.client.engine.okhttp.OkHttp 6 | 7 | actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(OkHttp, config) -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/com/qust/helper/Platform.ios.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper 2 | 3 | import platform.UIKit.UIDevice 4 | 5 | class IOSPlatform: Platform { 6 | override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion 7 | } 8 | 9 | actual fun getPlatform(): Platform = IOSPlatform() -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/qust/helper/ui/widget/Modifier.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.widget 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | 7 | @Composable 8 | actual fun Modifier.click(click: () -> Unit) = this.clickable(onClick = click) -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/layout/view_text.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/utils/EasUtils.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | object EasUtils { 4 | 5 | /** 6 | * 计算查询的学期参数 7 | */ 8 | fun getTermParam(entranceTime: Int, pickYear: Int): Pair{ 9 | return Pair( 10 | (pickYear / 2 + entranceTime).toString(), 11 | if(pickYear % 2 == 0) "3" else "12" 12 | ) 13 | } 14 | } -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/com/qust/helper/main.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper 2 | 3 | import androidx.compose.ui.ExperimentalComposeUiApi 4 | import androidx.compose.ui.window.ComposeViewport 5 | import kotlinx.browser.document 6 | 7 | @OptIn(ExperimentalComposeUiApi::class) 8 | fun main() { 9 | ComposeViewport(document.body!!) { 10 | App() 11 | } 12 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/MyViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | 7 | class MyViewModel: BaseViewModel() { 8 | 9 | var hasLogin by mutableStateOf(false) 10 | 11 | var userName by mutableStateOf("未登录") 12 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_electric.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/model/network/Exceptions.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.network 2 | 3 | import com.qust.helper.data.i18n.Strings 4 | 5 | class NeedLoginException : RuntimeException(Strings.MSG_NEED_LOGIN) 6 | 7 | class WrongAccountException: RuntimeException(Strings.MSG_ERROR_ACCOUNT) 8 | 9 | class WrongLogicException(msg: String) : RuntimeException(Strings.MSG_ERROR_LOGIC + ": $msg") -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | QustHelper 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import com.qust.helper.data.Pages 6 | import com.qust.helper.ui.page.BasePage 7 | 8 | class HomeViewModel: BaseViewModel() { 9 | 10 | val pages: List> by mutableStateOf(Pages.defaultPages) 11 | 12 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/qust/helper/ui/page/PageController.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.page 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.qust.helper.viewmodel.ApplicationViewModel 5 | 6 | @Composable 7 | actual fun rememberPageController(): PageController { 8 | return ApplicationViewModel 9 | } 10 | 11 | @Composable 12 | actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .kotlin 3 | .gradle 4 | **/build/ 5 | xcuserdata 6 | !src/**/build/ 7 | local.properties 8 | .idea 9 | .DS_Store 10 | .externalNativeBuild 11 | .cxx 12 | *.xcodeproj/* 13 | !*.xcodeproj/project.pbxproj 14 | !*.xcodeproj/xcshareddata/ 15 | !*.xcodeproj/project.xcworkspace/ 16 | !*.xcworkspace/contents.xcworkspacedata 17 | **/xcshareddata/WorkspaceSettings.xcsettings 18 | /bak 19 | **/*test* 20 | captures 21 | config.json -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/model/database/ExamStorage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.database 2 | 3 | import com.qust.helper.entity.eas.Exam 4 | 5 | expect fun getExamStorage(): ExamStorage 6 | 7 | interface ExamStorage { 8 | 9 | fun getExamsByTerm(term: Int): List = emptyList() 10 | 11 | fun setRead(id: Int){ } 12 | 13 | fun insertAll(exams: List){ } 14 | 15 | fun updateAll(exams: List){ } 16 | } 17 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/layout/widget_day_lesson.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/qust/helper/main.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper 2 | 3 | import androidx.compose.runtime.key 4 | import androidx.compose.ui.window.application 5 | import com.qust.helper.ui.theme.AppTheme 6 | import com.qust.helper.viewmodel.ApplicationViewModel 7 | 8 | fun main() = application { 9 | AppTheme { 10 | for (window in ApplicationViewModel.windows) { 11 | key(window) { 12 | window.Content() 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_login.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/ui/activity/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.activity 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import com.qust.helper.ui.page.HomePage 7 | 8 | class MainActivity: ComponentActivity() { 9 | 10 | val page = HomePage 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContent { HomePage.ComposePage() } 15 | } 16 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_arrow_left.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/model/database/MarkStorage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.database 2 | 3 | import com.qust.helper.entity.eas.Mark 4 | 5 | expect fun getMarkStorage(): MarkStorage 6 | 7 | interface MarkStorage { 8 | 9 | fun getMarksByKchId(id: String): List = emptyList() 10 | 11 | fun getMarksByIndex(index: Int): List = emptyList() 12 | 13 | fun updateAll(marks: List){ } 14 | 15 | fun insertAll(marks: List){ } 16 | 17 | fun setRead(id: Int){ } 18 | 19 | fun clear(index: Int){ } 20 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_visibility.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/xml/daily_lesson.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/page/PageController.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.page 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | interface PageController { 6 | 7 | /** 8 | * 打开一个新界面 9 | */ 10 | fun startPage(key: String) 11 | 12 | fun startPage(page: BasePage<*>) = startPage(page.key) 13 | 14 | /** 15 | * 返回上一级 16 | */ 17 | fun back() 18 | 19 | } 20 | 21 | @Composable 22 | expect fun rememberPageController(): PageController 23 | 24 | @Composable 25 | expect fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) 26 | -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import ComposeApp 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | MainViewControllerKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_water.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/utils/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | interface AbstractLogger { 4 | 5 | fun d(msg: String){ 6 | log("QustHelper", msg) 7 | } 8 | 9 | fun i(msg: String){ 10 | log("QustHelper", msg) 11 | } 12 | 13 | fun w(msg: String){ 14 | log("QustHelper", msg) 15 | } 16 | 17 | fun e(msg: String? = null, e: Throwable? = null){ 18 | log("QustHelper", msg, e) 19 | } 20 | 21 | fun log(tag: String, msg: String?, throwable: Throwable? = null) 22 | } 23 | 24 | expect fun getLogger(): AbstractLogger 25 | 26 | object Logger: AbstractLogger by getLogger() -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/ui/widget/Modifier.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.widget 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.material3.ripple 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | 10 | 11 | @Composable 12 | actual fun Modifier.click(click: () -> Unit) = this.clickable( 13 | interactionSource = remember { MutableInteractionSource() }, 14 | indication = ripple(), 15 | onClick = click 16 | ) -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_notification.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/qust/helper/ui/WindowPage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.window.Window 5 | import com.qust.helper.ui.page.BasePage 6 | import com.qust.helper.viewmodel.ApplicationViewModel 7 | 8 | class WindowPage( 9 | val page: BasePage<*> 10 | ) { 11 | 12 | @Composable 13 | fun Content() { 14 | Window( 15 | onCloseRequest = { ApplicationViewModel.close(this) }, 16 | title = page.title, 17 | ) { 18 | page.ComposePage() 19 | } 20 | } 21 | 22 | fun exit(){ 23 | 24 | } 25 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/model/database/LessonTableStorage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.database 2 | 3 | import com.qust.helper.entity.lesson.Lesson 4 | 5 | expect fun getLessonTableStorage(): LessonTableStorage 6 | 7 | interface LessonTableStorage { 8 | 9 | suspend fun getAllLesson(): List { 10 | return emptyList() 11 | } 12 | 13 | suspend fun saveLesson(lesson: Lesson): Long? { 14 | return -1 15 | } 16 | 17 | suspend fun updateLesson(lesson: Lesson): Boolean { 18 | return false 19 | } 20 | 21 | suspend fun mergeLesson(new: List, update: List, delete: List): Boolean { 22 | return false 23 | } 24 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/xml/drink_code.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/xml/term_lesson.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/utils/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | import android.util.Log 4 | 5 | 6 | actual fun getLogger(): AbstractLogger = LoggerImpl() 7 | 8 | class LoggerImpl: AbstractLogger { 9 | 10 | override fun d(msg: String) { 11 | Log.d("QustHelper", msg) 12 | } 13 | 14 | override fun i(msg: String) { 15 | Log.i("QustHelper", msg) 16 | } 17 | 18 | override fun w(msg: String) { 19 | Log.w("QustHelper", msg) 20 | } 21 | 22 | override fun e(msg: String?, e: Throwable?) { 23 | Log.e("QustHelper", msg, e) 24 | } 25 | 26 | override fun log(tag: String, msg: String?, throwable: Throwable?) { 27 | Log.i(tag, msg, throwable) 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_school.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/entity/eas/Notice.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.entity.eas 2 | 3 | import com.qust.helper.utils.JsonUtils.get 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonObject 6 | 7 | /** 8 | * 教务系统消息 9 | * @param id ID 10 | * @param time 创建时间 11 | * @param content 消息内容 12 | */ 13 | @Serializable 14 | data class Notice( 15 | val id: String = "", 16 | val time: String = "", 17 | val content: String = "", 18 | ){ 19 | companion object { 20 | fun createFromJson(js: JsonObject): Notice { 21 | return Notice( 22 | id = js["id", ""], 23 | time = js["cjsj", ""], 24 | content = js["xxnr", ""] 25 | ) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/utils/CodeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | object CodeUtils { 4 | 5 | val chars = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') 6 | 7 | fun byteToHexString(byteArray: ByteArray): String { 8 | val hexString = StringBuilder() 9 | for(b in byteArray) { 10 | val byte = b.toInt() and 0xFF 11 | hexString.append(chars[byte shr 4]) 12 | hexString.append(chars[byte and 0xF]) 13 | } 14 | return hexString.toString() 15 | } 16 | 17 | fun matcher(pattern: Regex, string: String, index: Int = 1): String? { 18 | val it = pattern.find(string) ?: return null 19 | return it.groups[index]?.value 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_insert_invitation.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/RequestViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel 2 | 3 | import com.qust.helper.viewmodel.extend.RequestAble 4 | import com.qust.helper.viewmodel.extend.RequestsImpl 5 | import com.qust.helper.viewmodel.extend.toastError 6 | 7 | open class RequestViewModel: BaseViewModel() { 8 | 9 | val request = RequestsImpl(this) 10 | 11 | fun request( 12 | block: suspend RequestAble.() -> Unit, 13 | onError: (Exception) -> Unit = { 14 | it.printStackTrace() 15 | toastError("网络错误: ${it.message}") 16 | } 17 | ) { 18 | runBackGround { 19 | try { 20 | request.request(block) 21 | }catch(e: Exception){ 22 | onError(e) 23 | } 24 | } 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /.fleet/receipt.json: -------------------------------------------------------------------------------- 1 | // Project generated by Kotlin Multiplatform Wizard 2 | { 3 | "spec": { 4 | "template_id": "kmt", 5 | "targets": { 6 | "android": { 7 | "ui": [ 8 | "compose" 9 | ] 10 | }, 11 | "ios": { 12 | "ui": [ 13 | "compose" 14 | ] 15 | }, 16 | "desktop": { 17 | "ui": [ 18 | "compose" 19 | ] 20 | }, 21 | "web": { 22 | "ui": [ 23 | "compose" 24 | ] 25 | } 26 | } 27 | }, 28 | "timestamp": "2025-01-27T06:13:49.058283733Z" 29 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_article.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_brightness.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/eas/BaseEasViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel.eas 2 | 3 | import androidx.compose.runtime.mutableIntStateOf 4 | import com.qust.helper.model.account.EasAccount 5 | import com.qust.helper.utils.EasUtils 6 | import com.qust.helper.viewmodel.RequestViewModel 7 | import com.qust.helper.viewmodel.extend.toastWarning 8 | 9 | open class BaseEasViewModel: RequestViewModel() { 10 | 11 | val pickYear = mutableIntStateOf(EasAccount.getCurrentGrade()) 12 | 13 | protected fun getPickTerm(): Pair{ 14 | val entranceDate = EasAccount.entranceDate 15 | if(entranceDate == -1) toastWarning("未设置入学年份,可能导致查询结果异常") 16 | val pick = pickYear.intValue 17 | return EasUtils.getTermParam(entranceDate, pick) 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 青科助手 3 | 欢迎使用 4 | 跳过 5 | 确定 6 | 取消 7 | 学期 8 | 查询 9 | 删除 10 | 第 %s 周 11 | 当前开学日期: %s\n查询到的开学日期: %s 12 | 保存课表 13 | 登录 14 | 学期课表 15 | 饮水码 16 | 正在检查更新 17 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/extend/LoadingAble.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel.extend 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | 7 | 8 | interface LoadingAble { 9 | val _loadingText: MutableState 10 | 11 | val loadingText: String 12 | 13 | fun showDialog(message: String) 14 | 15 | fun clearDialog() 16 | } 17 | 18 | class LoadingAbleImpl( 19 | override val _loadingText: MutableState = mutableStateOf("") 20 | ): LoadingAble { 21 | 22 | override val loadingText by _loadingText 23 | 24 | override fun showDialog(message: String) { 25 | _loadingText.value = message 26 | } 27 | override fun clearDialog() { 28 | _loadingText.value = "" 29 | } 30 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.qust.helper.ui.widget.toast.ToastUIState 6 | import com.qust.helper.viewmodel.extend.LoadingAble 7 | import com.qust.helper.viewmodel.extend.LoadingAbleImpl 8 | import com.qust.helper.viewmodel.extend.ToastAble 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.launch 11 | import kotlinx.coroutines.withContext 12 | 13 | open class BaseViewModel: ViewModel(), ToastAble, LoadingAble by LoadingAbleImpl() { 14 | 15 | override val toastData: ToastUIState = ToastUIState() 16 | 17 | fun runBackGround(block: suspend () -> Unit){ 18 | viewModelScope.launch { 19 | withContext(Dispatchers.IO){ 20 | block() 21 | } 22 | } 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/eas/QueryNoticeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel.eas 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import com.qust.helper.entity.eas.Notice 7 | import com.qust.helper.model.account.EasAccount 8 | import com.qust.helper.model.eas.NoticeModel 9 | 10 | class QueryNoticeViewModel: BaseEasViewModel() { 11 | 12 | var notices by mutableStateOf(emptyList()) 13 | 14 | var hasRefresh by mutableStateOf(false) 15 | var refreshing by mutableStateOf(false) 16 | 17 | fun queryNotice() { 18 | hasRefresh = true 19 | refreshing = true 20 | runBackGround { 21 | try { 22 | notices = NoticeModel.queryNotice(EasAccount, 1, 20) 23 | }finally { 24 | refreshing = false 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/room/dao/ExamMapper.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.room.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | import androidx.room.Update 7 | import com.qust.helper.room.entity.ExamDao 8 | 9 | @Dao 10 | interface ExamMapper { 11 | 12 | @Query("SELECT * FROM exams WHERE `term` = :term") 13 | fun selectByTerm(term: Int): List 14 | 15 | @Query("UPDATE exams SET isNew = 0 WHERE id = :id") 16 | fun setRead(id: Int) 17 | 18 | @Insert 19 | fun insert(lesson: ExamDao) 20 | 21 | @Insert 22 | fun insertAll(lessons: List) 23 | 24 | @Update 25 | fun update(lesson: ExamDao) 26 | 27 | @Update 28 | fun updateAll(lesson: List) 29 | 30 | @Query("DELETE FROM exams WHERE `term` = :term") 31 | fun clear(term: Int) 32 | 33 | @Query("DELETE FROM exams") 34 | fun delete() 35 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "QustHelper" 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | pluginManagement { 5 | repositories { 6 | google { 7 | mavenContent { 8 | includeGroupAndSubgroups("androidx") 9 | includeGroupAndSubgroups("com.android") 10 | includeGroupAndSubgroups("com.google") 11 | } 12 | } 13 | mavenCentral() 14 | gradlePluginPortal() 15 | } 16 | } 17 | 18 | dependencyResolutionManagement { 19 | repositories { 20 | google { 21 | mavenContent { 22 | includeGroupAndSubgroups("androidx") 23 | includeGroupAndSubgroups("com.android") 24 | includeGroupAndSubgroups("com.google") 25 | } 26 | } 27 | mavenCentral() 28 | } 29 | } 30 | 31 | include(":composeApp") -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.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.runtime.CompositionLocalProvider 7 | import androidx.compose.runtime.staticCompositionLocalOf 8 | 9 | val LocalColor = staticCompositionLocalOf { LightColors } 10 | 11 | @Composable 12 | fun AppTheme( 13 | darkTheme: Boolean = isSystemInDarkTheme(), 14 | content: @Composable () -> Unit 15 | ) { 16 | val colorScheme = when { 17 | darkTheme -> LightColors.colorScheme 18 | else -> LightColors.colorScheme 19 | } 20 | CompositionLocalProvider(LocalColor provides LightColors) { 21 | MaterialTheme( 22 | colorScheme = colorScheme, 23 | // typography = Typography, 24 | content = content 25 | ) 26 | } 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values-en/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | QustHelper 4 | Welcome 5 | Skip 6 | OK 7 | Cancel 8 | Term 9 | Query 10 | Delete 11 | %s Week 12 | Current start date: %s\nQueryed start date: %s 13 | Save Lesson Table 14 | Login 15 | Term Lesson 16 | Drink Code 17 | Checking for updates 18 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_view_list.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/platform/utils/SettingUtilsImpl.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.platform.utils 2 | 3 | abstract class SettingUtilsImpl { 4 | 5 | abstract fun getString(key: String, defValue: String): String? 6 | abstract fun getInt(key: String, defValue: Int): Int 7 | abstract fun getBoolean(key: String, defValue: Boolean): Boolean 8 | abstract fun getFloat(key: String, defValue: Float): Float 9 | abstract fun getLong(key: String, defValue: Long): Long 10 | abstract fun getStringSet(key: String, defValue: Set): Set? 11 | 12 | abstract fun putString(key: String, value: String) 13 | abstract fun putInt(key: String, value: Int) 14 | abstract fun putBoolean(key: String, value: Boolean) 15 | abstract fun putFloat(key: String, value: Float) 16 | abstract fun putLong(key: String, value: Long) 17 | abstract fun putStringSet(key: String, value: Set) 18 | 19 | abstract fun removeKey(key: String) 20 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/extend/RequestAble.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel.extend 2 | 3 | import kotlinx.coroutines.sync.Mutex 4 | import kotlinx.coroutines.sync.withLock 5 | 6 | 7 | interface RequestAble { 8 | suspend fun request(block: suspend RequestAble.() -> Unit) 9 | } 10 | 11 | /** 12 | * 控制联网加载时展示Dialog的类 13 | */ 14 | class RequestsImpl( 15 | private val dialogAble: LoadingAble, 16 | ): RequestAble { 17 | 18 | private var count = 0 19 | private val mutex = Mutex() 20 | 21 | override suspend fun request(block: suspend RequestAble.() -> Unit){ 22 | try { 23 | mutex.withLock { 24 | if(count == 0) dialogAble.showDialog("加载中") 25 | count++ 26 | } 27 | block() 28 | } catch(e: Exception) { 29 | throw e 30 | } finally { 31 | mutex.withLock { 32 | if(count > 0) { 33 | count-- 34 | if(count == 0) dialogAble.clearDialog() 35 | } 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/page/EmptyPage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.page 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material.Text 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.Home 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.lifecycle.viewmodel.compose.viewModel 12 | import com.qust.helper.viewmodel.BaseViewModel 13 | 14 | object EmptyPage: BasePage("空白页", Icons.Default.Home) { 15 | 16 | @Composable 17 | override fun Content(viewModel: BaseViewModel) { 18 | Box(Modifier.fillMaxSize()) { 19 | Text("this page is empty", Modifier.align(Alignment.Center)) 20 | } 21 | } 22 | 23 | @Composable 24 | override fun getViewModel() = viewModel() 25 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/room/entity/ExamDao.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.room.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.qust.helper.entity.eas.Exam 7 | 8 | @Entity(tableName = "exams") 9 | data class ExamDao( 10 | @PrimaryKey(autoGenerate = true) val id: Int = 0, 11 | 12 | @ColumnInfo val term: Int = 0, 13 | 14 | @ColumnInfo val name: String = "", 15 | @ColumnInfo val place: String = "", 16 | @ColumnInfo val time: String = "", 17 | 18 | @ColumnInfo val isNew: Int, 19 | ) 20 | 21 | fun Exam.toExamDao() = ExamDao( 22 | id = this.id, 23 | term = this.term, 24 | name = this.name, 25 | place = this.place, 26 | time = this.time, 27 | isNew = this.isNew, 28 | ) 29 | 30 | fun ExamDao.toExam() = Exam( 31 | id = this.id, 32 | term = this.term, 33 | name = this.name, 34 | place = this.place, 35 | time = this.time, 36 | isNew = this.isNew, 37 | ) -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_grid_view.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/model/database/ExamStorage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.database 2 | 3 | import com.qust.helper.entity.eas.Exam 4 | import com.qust.helper.room.AppDataBase 5 | import com.qust.helper.room.entity.ExamDao 6 | import com.qust.helper.room.entity.toExam 7 | import com.qust.helper.room.entity.toExamDao 8 | 9 | actual fun getExamStorage(): ExamStorage = ExamStorageImpl() 10 | 11 | class ExamStorageImpl: ExamStorage { 12 | 13 | override fun getExamsByTerm(term: Int): List { 14 | return AppDataBase.INSTANCE.examDao().selectByTerm(term).map(ExamDao::toExam) 15 | } 16 | 17 | override fun setRead(id: Int){ 18 | return AppDataBase.INSTANCE.examDao().setRead(id) 19 | } 20 | 21 | override fun insertAll(exams: List){ 22 | AppDataBase.INSTANCE.examDao().insertAll(exams.map(Exam::toExamDao)) 23 | } 24 | 25 | override fun updateAll(exams: List){ 26 | AppDataBase.INSTANCE.examDao().updateAll(exams.map(Exam::toExamDao)) 27 | } 28 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_visibility_off.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/extend/ToastAble.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel.extend 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.qust.helper.ui.widget.toast.ToastData 5 | import com.qust.helper.ui.widget.toast.ToastUIState 6 | import com.qust.helper.viewmodel.BaseViewModel 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.launch 9 | import kotlinx.coroutines.withContext 10 | 11 | interface ToastAble { 12 | val toastData: ToastUIState 13 | } 14 | 15 | fun BaseViewModel.toast(msg: String, type: ToastData.Type = ToastData.Type.NORMAL){ 16 | viewModelScope.launch { 17 | withContext(Dispatchers.IO) { 18 | toastData.show(ToastData(msg, type)) 19 | } 20 | } 21 | } 22 | 23 | fun BaseViewModel.toastOK(msg: String) = toast(msg, ToastData.Type.SUCCESS) 24 | 25 | fun BaseViewModel.toastWarning(msg: String) = toast(msg, ToastData.Type.WARNING) 26 | 27 | fun BaseViewModel.toastError(msg: String) = toast(msg, ToastData.Type.ERROR) -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/com/qust/helper/utils/SettingUtils.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | import com.qust.helper.platform.utils.SettingUtilsImpl 4 | 5 | class SettingImpl : SettingUtilsImpl() { 6 | 7 | override fun getString(key: String, defValue: String) = defValue 8 | override fun getInt(key: String, defValue: Int) = defValue 9 | override fun getBoolean(key: String, defValue: Boolean) = defValue 10 | override fun getFloat(key: String, defValue: Float) = defValue 11 | override fun getStringSet(key: String, defValue: Set): Set? = defValue 12 | 13 | override fun putString(key: String, value: String){ } 14 | override fun putInt(key: String, value: Int) { } 15 | override fun putBoolean(key: String, value: Boolean) { } 16 | override fun putFloat(key: String, value: Float) { } 17 | override fun putStringSet(key: String, value: Set) { } 18 | 19 | override fun removeKey(key: String){ } 20 | } 21 | 22 | actual fun getSettingUtils(): SettingUtilsImpl = SettingImpl() -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/account/AccountManagerViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel.account 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import com.qust.helper.model.account.EasAccount 7 | import com.qust.helper.model.account.IPassAccount 8 | import com.qust.helper.viewmodel.BaseViewModel 9 | 10 | class AccountManagerViewModel: BaseViewModel() { 11 | 12 | var easAccountName by mutableStateOf(EasAccount.getAccountName()) 13 | var iPassAccountName by mutableStateOf(IPassAccount.getAccountName()) 14 | 15 | var askForLogoutEas by mutableStateOf(false) 16 | var askForLogoutIpass by mutableStateOf(false) 17 | 18 | fun easLogout(){ 19 | runBackGround { 20 | EasAccount.logout() 21 | easAccountName = "" 22 | } 23 | } 24 | 25 | fun ipassLogout(){ 26 | runBackGround { 27 | IPassAccount.logout() 28 | iPassAccountName = "" 29 | } 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/model/SettingModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import com.qust.helper.data.Keys 5 | import com.qust.helper.utils.DateUtils 6 | import com.qust.helper.utils.SettingUtils 7 | import java.io.File 8 | 9 | object SettingModel { 10 | 11 | 12 | val themeDark = mutableStateOf(SettingUtils[Keys.KEY_THEME_DARK, false]) 13 | 14 | val themeFollowSystem = mutableStateOf(SettingUtils[Keys.KEY_THEME_FOLLOW_SYSTEM, true]) 15 | 16 | val lessonTableFolder = File("") 17 | 18 | 19 | var totalWeek = SettingUtils[Keys.SETTING_TOTAL_WEEK, 1] 20 | set(value) { 21 | SettingUtils[Keys.SETTING_TOTAL_WEEK] = value 22 | field = value 23 | } 24 | 25 | var startDay = SettingUtils.getString(Keys.SETTING_START_DAY, "").let { 26 | if(it.isBlank()) DateUtils.today() else DateUtils.YMD.parse(it) 27 | } 28 | set(value) { 29 | SettingUtils[Keys.SETTING_START_DAY] = DateUtils.YMD.format(value) 30 | field = value 31 | } 32 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/entity/lesson/Lesson.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.entity.lesson 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * 一节课程 7 | * @param id 数据库ID 8 | * @param type 课程类型 0: auto 自动添加的课程; 1: user 用户创建的课程 9 | * @param reference 对另一个课程的引用,修改该课程会影响本课程 10 | * @param lessonId 课程ID,用于自动创建的课程标识唯一ID 11 | * @param colorLabel 课程颜色,内置颜色的索引 12 | * @param weeks 第几周有课, long形式的boolean数组 13 | * @param week 周几(从0开始) 14 | * @param startMinute 上课时间 15 | * @param endMinute 下课时间 16 | * @param name 课程名称 17 | * @param place 课程教室 18 | * @param teacher 课程教师 19 | * @param remark 备注 20 | */ 21 | @Serializable 22 | data class Lesson( 23 | val id: Long = 0L, 24 | val type: Int = 0, 25 | val reference: Long = 0L, 26 | val lessonId: String = "", 27 | val colorLabel: Int = 0, 28 | val weeks: Long = 0L, 29 | val week: Int = 0, 30 | val startMinute: Int = 0, 31 | val endMinute: Int = 0, 32 | val name: String = "", 33 | val place: String = "", 34 | val teacher: String = "", 35 | val remark: String = "" 36 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/model/database/AcademicStorage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.database 2 | 3 | import com.qust.helper.entity.eas.AcademicGroup 4 | import com.qust.helper.entity.eas.AcademicInfo 5 | 6 | 7 | expect fun getAcademicStorage(): AcademicStorage 8 | 9 | interface AcademicStorage { 10 | 11 | suspend fun getInfo(): List = emptyList() 12 | 13 | /** 14 | * 获取建议修读年份为指定值的课程 15 | */ 16 | suspend fun getInfoByTerm(term: Int): List = emptyList() 17 | 18 | /** 19 | * 获取课程类型为指定值的课程 20 | */ 21 | suspend fun getInfoByGroup(group: Int): List = emptyList() 22 | 23 | 24 | suspend fun getGroups(): List = emptyList() 25 | 26 | /** 27 | * 获取按照建议修读年份排序的课程组 28 | */ 29 | suspend fun getGroupsByTerm(): List = emptyList() 30 | 31 | 32 | suspend fun insertInfo(info: List){ } 33 | 34 | suspend fun insertGroups(groups: List){ } 35 | 36 | suspend fun clearInfo(){ } 37 | 38 | suspend fun clearGroups(){ } 39 | } 40 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/layout/layout_tips.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 19 | 20 | 27 | 28 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/widget/lesson/lessonEdit/LessonEditUIState.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.widget.lesson.lessonEdit 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.compose.runtime.toMutableStateList 7 | import com.qust.helper.model.lessonTable.LessonTableModel 8 | 9 | class LessonEditUIState { 10 | 11 | val totalWeek by LessonTableModel._totalWeek 12 | 13 | var lessonName by mutableStateOf("") 14 | var lessonPlace by mutableStateOf("") 15 | var lessonTeacher by mutableStateOf("") 16 | 17 | var colorIndex by mutableStateOf(0) 18 | 19 | val timeTable by LessonTableModel._timeTable 20 | 21 | val week = mutableStateOf(0) 22 | 23 | val startHour = mutableStateOf("") 24 | val startMinute = mutableStateOf("") 25 | 26 | val endHour = mutableStateOf("") 27 | val endMinute = mutableStateOf("") 28 | 29 | val weeks = List(totalWeek){ false }.toMutableStateList() 30 | } 31 | 32 | 33 | interface LessonEditUIEvent { 34 | fun saveLesson() 35 | fun cancel() 36 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/room/dao/MarkMapper.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.room.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | import androidx.room.Update 7 | import com.qust.helper.room.entity.MarkDao 8 | 9 | @Dao 10 | interface MarkMapper { 11 | 12 | @Query("SELECT * FROM marks WHERE `kchId` = :id") 13 | fun selectByKchId(id: String): List 14 | 15 | @Query("SELECT * FROM marks WHERE `index` = :index") 16 | fun selectByIndex(index: Int): List 17 | 18 | @Query("UPDATE marks SET isNew = 0 WHERE id = :id") 19 | fun setRead(id: Int) 20 | 21 | @Query("SELECT COUNT(1) FROM marks WHERE `index` = :index") 22 | fun countByIndex(index: Int): Int 23 | 24 | @Insert 25 | fun insert(lesson: MarkDao) 26 | 27 | @Insert 28 | fun insertAll(lessons: List) 29 | 30 | @Update 31 | fun update(lesson: MarkDao) 32 | 33 | @Update 34 | fun updateAll(lesson: List) 35 | 36 | @Query("DELETE FROM marks WHERE `index` = :index") 37 | fun clear(index: Int) 38 | 39 | @Query("DELETE FROM marks") 40 | fun delete() 41 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/entity/eas/Exam.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.entity.eas 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * 考试安排 7 | * @param term 第几学期 8 | * @param name 课程名称 9 | * @param place 考试地点 10 | * @param time 考试时间 11 | */ 12 | @Serializable 13 | data class Exam( 14 | val id: Int = 0, 15 | 16 | val term: Int = 0, 17 | 18 | val name: String = "", 19 | val place: String = "", 20 | val time: String = "", 21 | 22 | val isNew: Int = 1, 23 | ){ 24 | 25 | override fun equals(other: Any?): Boolean { 26 | if(this === other) return true 27 | if(javaClass != other?.javaClass) return false 28 | 29 | other as Exam 30 | 31 | if(term != other.term) return false 32 | if(name != other.name) return false 33 | if(place != other.place) return false 34 | if(time != other.time) return false 35 | 36 | return true 37 | } 38 | 39 | override fun hashCode(): Int { 40 | var result = term 41 | result = 31 * result + name.hashCode() 42 | result = 31 * result + place.hashCode() 43 | result = 31 * result + time.hashCode() 44 | return result 45 | } 46 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/qust/helper/viewmodel/ApplicationViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel 2 | 3 | import androidx.compose.runtime.mutableStateListOf 4 | import com.qust.helper.data.Pages 5 | import com.qust.helper.ui.WindowPage 6 | import com.qust.helper.ui.page.EmptyPage 7 | import com.qust.helper.ui.page.HomePage 8 | import com.qust.helper.ui.page.PageController 9 | 10 | object ApplicationViewModel: PageController { 11 | 12 | private val _windows = mutableStateListOf( 13 | WindowPage(HomePage) 14 | ) 15 | 16 | val windows: List get() = _windows 17 | 18 | override fun startPage(key: String) { 19 | val page = Pages[key] ?: EmptyPage 20 | _windows.add( 21 | WindowPage( 22 | page = page, 23 | ) 24 | ) 25 | } 26 | 27 | override fun back() { 28 | val item = _windows.last() 29 | item.exit() 30 | _windows.removeLast() 31 | } 32 | 33 | 34 | fun close(windowPage: WindowPage){ 35 | windowPage.exit() 36 | _windows.remove(windowPage) 37 | } 38 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | qust 3 |

4 | 5 |
6 | 7 | # 青科助手 8 | 9 | ✨ 一款集教务查询和课表管理于一体的移动端软件 ✨ 10 |
11 | 12 |

13 | 14 | license 15 | 16 | 17 | release 18 | 19 |

20 | 21 | ### 简介 📃 22 | 23 | - Qust Helper 是一款移动端教务系统客户端,旨在为师生提供一个便捷的教务访问方式,随时随地查看课程安排、考试成绩、教务通知等信息。 24 | - Qust Helper 还是一款优秀的课表软件,能够轻松从教务导入课程信息,自动生成个性化的课表。 25 | - 基于全新的 Jetpack Compose 架构,拥有更加出色的用户体验。*后续可能支持多平台*。 26 | 27 | ### 应用截图📸 28 | 29 | ![](captures/Screenshot_20240322_163254.png) ![](captures/Screenshot_20240322_163017.png) ![](captures/Screenshot_20240322_163159.png) 30 | 31 | 32 | ### 功能实现 💻 33 | 34 | - [x] 教务课表导入 35 | - [x] 成绩查询 36 | - [x] 学业情况查询 37 | - [x] 考试查询 38 | - [x] 教务通知推送 39 | - [x] 成绩推送 40 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/ui/activity/PageActivity.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.activity 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import com.qust.helper.data.Pages 9 | import com.qust.helper.ui.page.BasePage 10 | import com.qust.helper.ui.page.EmptyPage 11 | 12 | class PageActivity: ComponentActivity() { 13 | 14 | companion object { 15 | fun startActivity(context: Context, page: String){ 16 | context.startActivity(Intent(context, PageActivity::class.java).putExtra("page", page)) 17 | } 18 | } 19 | 20 | lateinit var page: BasePage<*> 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | val page = Pages[intent.getStringExtra("page")] 25 | if(page == null) { 26 | this.page = EmptyPage 27 | }else{ 28 | this.page = page 29 | } 30 | 31 | setContent { this.page.ComposePage() } 32 | } 33 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/qust/helper/utils/SettingUtils.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | import com.qust.helper.platform.utils.SettingUtilsImpl 4 | 5 | class SettingImpl : SettingUtilsImpl() { 6 | 7 | override fun getString(key: String, defValue: String) = defValue 8 | override fun getInt(key: String, defValue: Int) = defValue 9 | override fun getBoolean(key: String, defValue: Boolean) = defValue 10 | override fun getFloat(key: String, defValue: Float) = defValue 11 | override fun getLong(key: String, defValue: Long) = defValue 12 | override fun getStringSet(key: String, defValue: Set): Set? = defValue 13 | 14 | override fun putString(key: String, value: String){ } 15 | override fun putInt(key: String, value: Int) { } 16 | override fun putBoolean(key: String, value: Boolean) { } 17 | override fun putFloat(key: String, value: Float) { } 18 | override fun putLong(key: String, value: Long) { } 19 | override fun putStringSet(key: String, value: Set) { } 20 | 21 | override fun removeKey(key: String){ } 22 | } 23 | 24 | actual fun getSettingUtils(): SettingUtilsImpl = SettingImpl() -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/room/dao/LessonMapper.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.room.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.Query 7 | import androidx.room.Transaction 8 | import androidx.room.Update 9 | import com.qust.helper.room.entity.LessonDao 10 | 11 | @Dao 12 | interface LessonMapper { 13 | 14 | @Query("SELECT * FROM lesson") 15 | fun selectAll(): List 16 | 17 | @Insert 18 | fun insert(lesson: LessonDao): Long? 19 | 20 | @Insert 21 | fun insertAll(lessons: List) 22 | 23 | @Update 24 | fun update(lesson: LessonDao): Int 25 | 26 | @Update 27 | fun updateAll(lessons: List): Int 28 | 29 | @Delete 30 | fun delete(lesson: LessonDao) 31 | 32 | @Delete 33 | fun deleteAll(lessons: List) 34 | 35 | 36 | @Query("DELETE FROM lesson WHERE 1") 37 | fun clearTable() 38 | 39 | @Transaction 40 | open fun mergeLesson(new: List, update: List, delete: List) { 41 | insertAll(new) 42 | assert(updateAll(update) == update.size) 43 | deleteAll(delete) 44 | } 45 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/widget/toast/ToastUIState.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.widget.toast 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import kotlinx.coroutines.CancellableContinuation 8 | import kotlinx.coroutines.delay 9 | import kotlinx.coroutines.suspendCancellableCoroutine 10 | import kotlinx.coroutines.sync.Mutex 11 | import kotlinx.coroutines.sync.withLock 12 | import kotlin.coroutines.resume 13 | 14 | @Stable 15 | class ToastUIState { 16 | private val mutex = Mutex() 17 | 18 | var currentData: ToastData? by mutableStateOf(null) 19 | private set 20 | 21 | private var continuation: CancellableContinuation? = null 22 | 23 | suspend fun show(toastData: ToastData): Unit = mutex.withLock { 24 | try { 25 | suspendCancellableCoroutine { continuation -> 26 | this.continuation = continuation 27 | currentData = toastData 28 | } 29 | } finally { 30 | currentData = null 31 | } 32 | } 33 | 34 | suspend fun run() { 35 | delay(2000L) 36 | continuation?.resume(Unit) 37 | } 38 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/widget/components/Texts.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.widget.components 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.vector.ImageVector 12 | import androidx.compose.ui.unit.dp 13 | import com.qust.helper.ui.theme.colorSecondaryText 14 | 15 | @Composable 16 | fun IconText( 17 | text: String, 18 | icon: ImageVector, 19 | modifier: Modifier = Modifier, 20 | iconModifier: Modifier = Modifier 21 | ) { 22 | Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically){ 23 | Text( 24 | text = text, 25 | style = MaterialTheme.typography.bodySmall, 26 | modifier = Modifier.padding(start = 8.dp), 27 | color = colorSecondaryText, 28 | ) 29 | Icon(imageVector = icon, modifier = iconModifier, contentDescription = null, tint = colorSecondaryText) 30 | } 31 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/model/database/MarkStorage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.database 2 | 3 | import com.qust.helper.entity.eas.Mark 4 | import com.qust.helper.room.AppDataBase 5 | import com.qust.helper.room.entity.MarkDao 6 | import com.qust.helper.room.entity.toMark 7 | import com.qust.helper.room.entity.toMarkDao 8 | 9 | actual fun getMarkStorage(): MarkStorage = MarkStorageImpl() 10 | 11 | class MarkStorageImpl: MarkStorage { 12 | 13 | override fun getMarksByKchId(id: String): List { 14 | return AppDataBase.INSTANCE.markDao().selectByKchId(id).map(MarkDao::toMark) 15 | } 16 | 17 | override fun getMarksByIndex(index: Int): List { 18 | return AppDataBase.INSTANCE.markDao().selectByIndex(index).map(MarkDao::toMark) 19 | } 20 | 21 | override fun updateAll(marks: List){ 22 | return AppDataBase.INSTANCE.markDao().updateAll(marks.map(Mark::toMarkDao)) 23 | } 24 | 25 | override fun insertAll(marks: List){ 26 | return AppDataBase.INSTANCE.markDao().insertAll(marks.map(Mark::toMarkDao)) 27 | } 28 | 29 | override fun setRead(id: Int) { 30 | return AppDataBase.INSTANCE.markDao().setRead(id) 31 | } 32 | 33 | override fun clear(index: Int) { 34 | return AppDataBase.INSTANCE.markDao().clear(index) 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/drawables/Electric.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.drawables 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val Drawables.Electric: ImageVector 10 | get() { 11 | if (_Electric != null) { 12 | return _Electric!! 13 | } 14 | _Electric = ImageVector.Builder( 15 | name = "Electric", 16 | defaultWidth = 24.dp, 17 | defaultHeight = 24.dp, 18 | viewportWidth = 1024f, 19 | viewportHeight = 1024f 20 | ).apply { 21 | path(fill = SolidColor(Color(0xFFFFFFFF))) { 22 | moveTo(735.3f, 102.5f) 23 | lineTo(250.1f, 521f) 24 | lineToRelative(260.1f, 82.9f) 25 | lineToRelative(-203.1f, 317.6f) 26 | lineToRelative(519.1f, -384.4f) 27 | lineToRelative(-272.7f, -80.3f) 28 | } 29 | }.build() 30 | 31 | return _Electric!! 32 | } 33 | 34 | @Suppress("ObjectPropertyName") 35 | private var _Electric: ImageVector? = null 36 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/qust/helper/utils/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | import java.io.PrintWriter 4 | import java.io.StringWriter 5 | 6 | 7 | actual fun getLogger(): AbstractLogger = LoggerImpl() 8 | 9 | class LoggerImpl: AbstractLogger { 10 | 11 | override fun d(msg: String) { 12 | println("[QustHelper]\u001b[36m[D]\u001b[0m $msg") 13 | } 14 | 15 | override fun i(msg: String) { 16 | println("[QustHelper]\u001b[32m[I]\u001b[0m $msg") 17 | } 18 | 19 | override fun w(msg: String) { 20 | println("[QustHelper]\u001b[33m[W]\u001b[0m $msg") 21 | } 22 | 23 | override fun e(msg: String?, e: Throwable?) { 24 | if(e == null){ 25 | println("[QustHelper]\u001b[31m[E]\u001b[0m $msg") 26 | }else{ 27 | val writer = StringWriter() 28 | e.printStackTrace(PrintWriter(writer)) 29 | println("[QustHelper]\u001b[31m[E]\u001b[0m $msg ${e.message}\n $writer") 30 | } 31 | } 32 | 33 | override fun log(tag: String, msg: String?, throwable: Throwable?) { 34 | if(throwable == null){ 35 | println("[${tag}]\u001b[31m[E]\u001b[0m $msg") 36 | }else{ 37 | val writer = StringWriter() 38 | throwable.printStackTrace(PrintWriter(writer)) 39 | println("[QustHelper]\u001b[31m[E]\u001b[0m $msg ${throwable.message}\n $writer") 40 | } 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/ui/page/PageController.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.page 2 | 3 | import android.content.Context 4 | import androidx.activity.OnBackPressedDispatcher 5 | import androidx.activity.compose.LocalOnBackPressedDispatcherOwner 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.platform.LocalContext 9 | import com.qust.helper.ui.activity.PageActivity 10 | 11 | class AndroidPageController( 12 | val context: Context, 13 | private val dispatcher: OnBackPressedDispatcher? 14 | ): PageController { 15 | 16 | override fun startPage(key: String) { 17 | PageActivity.startActivity(context, key) 18 | } 19 | 20 | override fun back() { 21 | dispatcher?.onBackPressed() 22 | } 23 | 24 | } 25 | 26 | @Composable 27 | actual fun rememberPageController(): PageController { 28 | val context = LocalContext.current 29 | val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current 30 | 31 | return remember { AndroidPageController(context, backDispatcherOwner?.onBackPressedDispatcher) } 32 | } 33 | 34 | @Composable 35 | actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { 36 | androidx.activity.compose.BackHandler(enabled, onBack) 37 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/App.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper 2 | 3 | import android.app.Application 4 | import android.os.Looper 5 | import android.widget.Toast 6 | import com.qust.helper.room.AppDataBase 7 | import com.qust.helper.utils.UmengUtils 8 | import com.tencent.mmkv.MMKV 9 | 10 | 11 | class App: Application() { 12 | 13 | override fun onCreate() { 14 | super.onCreate() 15 | 16 | MMKV.initialize(this) 17 | 18 | AppDataBase.init(this) 19 | 20 | if(!BuildConfig.DEBUG) UmengUtils.init(this) 21 | 22 | Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(this, Thread.getDefaultUncaughtExceptionHandler())) 23 | } 24 | 25 | fun toast(message: String) { 26 | object : Thread() { 27 | override fun run() { 28 | Looper.prepare() 29 | Toast.makeText(this@App, message, Toast.LENGTH_LONG).show() 30 | Looper.loop() 31 | } 32 | }.start() 33 | } 34 | } 35 | 36 | private class ExceptionHandler(val app: App, val handler: Thread.UncaughtExceptionHandler?) : Thread.UncaughtExceptionHandler { 37 | override fun uncaughtException(thread: Thread, throwable: Throwable) { 38 | app.toast("应用发生错误,错误类型:" + throwable.javaClass) 39 | // LogUtil.Log("-------应用异常退出-------", throwable) 40 | // LogUtil.debugLog("-------应用异常退出-------\n") 41 | handler?.uncaughtException(thread, throwable) 42 | } 43 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/model/database/LessonTableModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.database 2 | 3 | import com.qust.helper.entity.lesson.Lesson 4 | import com.qust.helper.room.AppDataBase 5 | import com.qust.helper.room.entity.LessonDao 6 | import com.qust.helper.room.entity.toLesson 7 | import com.qust.helper.room.entity.toLessonDao 8 | 9 | actual fun getLessonTableStorage(): LessonTableStorage = LessonTableStorageImpl() 10 | 11 | class LessonTableStorageImpl: LessonTableStorage { 12 | 13 | override suspend fun getAllLesson(): List { 14 | return AppDataBase.INSTANCE.lessonDao().selectAll().map(LessonDao::toLesson) 15 | } 16 | 17 | override suspend fun saveLesson(lesson: Lesson): Long? { 18 | return AppDataBase.INSTANCE.lessonDao().insert(lesson.toLessonDao()) 19 | } 20 | 21 | override suspend fun updateLesson(lesson: Lesson): Boolean { 22 | return AppDataBase.INSTANCE.lessonDao().update(lesson.toLessonDao()) == 1 23 | } 24 | 25 | override suspend fun mergeLesson(new: List, update: List, delete: List): Boolean { 26 | try{ 27 | AppDataBase.INSTANCE.lessonDao().mergeLesson(new.map(Lesson::toLessonDao), update.map(Lesson::toLessonDao), delete.map(Lesson::toLessonDao)) 28 | return true 29 | }catch(e: Exception){ 30 | return false 31 | } 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/utils/UmengUtils.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | import android.content.Context 4 | import com.qust.helper.BuildConfig 5 | import com.qust.helper.data.Keys 6 | import com.umeng.analytics.MobclickAgent 7 | import com.umeng.commonsdk.UMConfigure 8 | 9 | /** 10 | * 友盟的工具类 11 | */ 12 | object UmengUtils { 13 | 14 | private var currentPage: String? = null 15 | 16 | fun init(context: Context){ 17 | if(BuildConfig.UMENG_APP_KEY.isNotEmpty()){ 18 | UMConfigure.setLogEnabled(BuildConfig.DEBUG) 19 | UMConfigure.preInit(context.applicationContext, BuildConfig.UMENG_APP_KEY, BuildConfig.UMENG_APP_CHANNEL) 20 | if(!SettingUtils.getBoolean(Keys.IS_FIRST_USE, true)) { 21 | UMConfigure.init(context.applicationContext, BuildConfig.UMENG_APP_KEY, BuildConfig.UMENG_APP_CHANNEL, UMConfigure.DEVICE_TYPE_PHONE, "") 22 | MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.MANUAL) 23 | } 24 | } 25 | } 26 | 27 | /** 28 | * 页面路由监听 29 | */ 30 | fun route(context: Context, name: String?){ 31 | if(currentPage == name || name == "home") return 32 | 33 | if(currentPage != null){ 34 | MobclickAgent.onPageEnd(currentPage) 35 | } 36 | 37 | if(name != null){ 38 | // MobclickAgent.onEventObject(context, "Navigation", mapOf("route" to name)) 39 | MobclickAgent.onPageStart(name) 40 | } 41 | currentPage = name 42 | } 43 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/account/IpassLoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel.account 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import com.qust.helper.data.Keys 7 | import com.qust.helper.model.account.IPassAccount 8 | import com.qust.helper.utils.SettingUtils 9 | import com.qust.helper.viewmodel.RequestViewModel 10 | import com.qust.helper.viewmodel.extend.toastError 11 | import com.qust.helper.viewmodel.extend.toastOK 12 | 13 | class IpassLoginViewModel : RequestViewModel() { 14 | 15 | val account = mutableStateOf(SettingUtils[Keys.IPASS_ACCOUNT, ""]) 16 | val password = mutableStateOf(SettingUtils[Keys.IPASS_PASSWORD, ""]) 17 | 18 | var accountError by mutableStateOf("") 19 | var passwordError by mutableStateOf("") 20 | 21 | fun login(onLogin: () -> Unit){ 22 | if(account.value.isEmpty()) { accountError = "请输入学号"; return } 23 | if(account.value.length < 2){ accountError = "学号格式错误"; return } 24 | if(password.value.isEmpty()) { passwordError = "请输入密码"; return } 25 | 26 | accountError = "" 27 | passwordError = "" 28 | 29 | request({ 30 | val result = IPassAccount.login(account.value, password.value, true) 31 | if(result){ 32 | toastOK("登录成功") 33 | onLogin() 34 | }else{ 35 | toastError("用户名或密码错误") 36 | } 37 | }) 38 | } 39 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/room/AppDataBase.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.room 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import com.qust.helper.room.dao.AcademicMapper 8 | import com.qust.helper.room.dao.ExamMapper 9 | import com.qust.helper.room.dao.LessonMapper 10 | import com.qust.helper.room.dao.MarkMapper 11 | import com.qust.helper.room.entity.AcademicGroupDao 12 | import com.qust.helper.room.entity.AcademicInfoDao 13 | import com.qust.helper.room.entity.ExamDao 14 | import com.qust.helper.room.entity.LessonDao 15 | import com.qust.helper.room.entity.MarkDao 16 | 17 | 18 | @Database( 19 | version = 1, 20 | exportSchema = false, 21 | entities = [ 22 | LessonDao::class, 23 | MarkDao::class, 24 | ExamDao::class, 25 | AcademicInfoDao::class, 26 | AcademicGroupDao::class, 27 | ] 28 | ) 29 | abstract class AppDataBase : RoomDatabase() { 30 | 31 | abstract fun lessonDao(): LessonMapper 32 | 33 | abstract fun markDao(): MarkMapper 34 | 35 | abstract fun examDao(): ExamMapper 36 | 37 | abstract fun academicDao(): AcademicMapper 38 | 39 | companion object { 40 | lateinit var INSTANCE: AppDataBase 41 | 42 | fun init(context: Context) { 43 | INSTANCE = Room.databaseBuilder( 44 | context.applicationContext, 45 | AppDataBase::class.java, 46 | "app_data" 47 | ).build() 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/data/i18n/Strings.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.data.i18n 2 | 3 | import androidx.compose.ui.text.intl.Locale 4 | import com.qust.helper.Res 5 | import kotlinx.coroutines.GlobalScope 6 | import kotlinx.coroutines.launch 7 | import org.jetbrains.compose.resources.ExperimentalResourceApi 8 | 9 | open class I18nStrings { 10 | 11 | open val TEXT_OK = "确定" 12 | open val TEXT_CANCEL = "取消" 13 | 14 | open val TEXT_LOGIN = "登录" 15 | 16 | open val TEXT_TERM = "学期" 17 | open val TEXT_SAVE_LESSON_TABLE = "保存课表" 18 | 19 | open val TEXT_NEW = "新" 20 | 21 | 22 | open val MSG_NEED_LOGIN: String = "需要登录" 23 | 24 | open val MSG_ERROR_ACCOUNT: String = "用户名或密码错误" 25 | 26 | open val MSG_ERROR_LOGIC: String = "逻辑错误" 27 | 28 | open val MSG_QUERY_TERM_START_TIME = "当前开学日期: %s\n查询到的开学日期: %s" 29 | 30 | 31 | open val ARRAY_WEEK_NAME = listOf("周一", "周二", "周三", "周四", "周五", "周六", "周日") 32 | 33 | open val ARRAY_TERM_NAME = listOf( 34 | "大一 上学期", "大一 下学期", 35 | "大二 上学期", "大二 下学期", 36 | "大三 上学期", "大三 下学期", 37 | "大四 上学期", "大四 下学期" 38 | ) 39 | 40 | open val ARRAY_QUERY_LESSON_TYPE = listOf("个人课表", "班级课表") 41 | 42 | } 43 | 44 | @OptIn(ExperimentalResourceApi::class) 45 | class I18nStringBuilder( 46 | /** ISO 639 语言代码 */ 47 | val code: String = Locale.current.language 48 | ){ 49 | init { 50 | GlobalScope.launch { 51 | Res.readBytes("/i18n/strings_${code}.properties") 52 | } 53 | } 54 | } 55 | 56 | val Strings: I18nStrings = I18nStrings() -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/utils/SettingUtils.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | import com.qust.helper.platform.utils.SettingUtilsImpl 4 | import com.tencent.mmkv.MMKV 5 | 6 | class MmkvSettingImpl : SettingUtilsImpl() { 7 | 8 | private val mmkv = MMKV.defaultMMKV() 9 | 10 | override fun getString(key: String, defValue: String) = mmkv.getString(key, defValue) 11 | override fun getInt(key: String, defValue: Int) = mmkv.getInt(key, defValue) 12 | override fun getBoolean(key: String, defValue: Boolean) = mmkv.getBoolean(key, defValue) 13 | override fun getFloat(key: String, defValue: Float) = mmkv.getFloat(key, defValue) 14 | override fun getLong(key: String, defValue: Long) = mmkv.getLong(key, defValue) 15 | override fun getStringSet(key: String, defValue: Set): Set? = mmkv.getStringSet(key, defValue) 16 | 17 | override fun putString(key: String, value: String){ mmkv.putString(key, value) } 18 | override fun putInt(key: String, value: Int) { mmkv.putInt(key, value) } 19 | override fun putBoolean(key: String, value: Boolean) { mmkv.putBoolean(key, value) } 20 | override fun putFloat(key: String, value: Float) { mmkv.putFloat(key, value) } 21 | override fun putLong(key: String, value: Long) { mmkv.putLong(key, value) } 22 | override fun putStringSet(key: String, value: Set) { mmkv.putStringSet(key, value) } 23 | 24 | override fun removeKey(key: String) = mmkv.removeValueForKey(key) 25 | } 26 | 27 | actual fun getSettingUtils(): SettingUtilsImpl = MmkvSettingImpl() -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/page/BasePage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.page 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material3.Scaffold 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.vector.ImageVector 10 | import com.qust.helper.ui.widget.components.LoadingUI 11 | import com.qust.helper.ui.widget.layout.AppContentWithBack 12 | import com.qust.helper.ui.widget.toast.ToastUI 13 | import com.qust.helper.viewmodel.BaseViewModel 14 | 15 | abstract class BasePage( 16 | val title: String, 17 | val icon: ImageVector, 18 | ) { 19 | 20 | val key: String = javaClass.name 21 | 22 | @Composable 23 | abstract fun getViewModel(): T 24 | 25 | @Composable 26 | open fun ComposePage() { 27 | val pageController = rememberPageController() 28 | Scaffold { contentPadding -> 29 | AppContentWithBack(title = title, contentPadding = contentPadding, onBack = pageController::back) { 30 | BaseContent() 31 | } 32 | } 33 | } 34 | 35 | @Composable 36 | open fun BaseContent() { 37 | val viewModel = getViewModel() 38 | Box(modifier = Modifier.fillMaxSize()) { 39 | Content(viewModel) 40 | ToastUI(viewModel.toastData, Modifier.align(Alignment.Center)) 41 | LoadingUI(viewModel) 42 | } 43 | } 44 | 45 | @Composable 46 | abstract fun Content(viewModel: T) 47 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/model/eas/ExamModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.eas 2 | 3 | import com.qust.helper.data.QustApi 4 | import com.qust.helper.entity.eas.Exam 5 | import com.qust.helper.model.account.EasAccount 6 | import com.qust.helper.model.database.ExamStorage 7 | import com.qust.helper.model.database.getExamStorage 8 | import com.qust.helper.utils.JSONArray 9 | import com.qust.helper.utils.JsonUtils 10 | import com.qust.helper.utils.JsonUtils.get 11 | import io.ktor.client.request.forms.FormDataContent 12 | import io.ktor.client.request.setBody 13 | import io.ktor.http.parameters 14 | import kotlinx.serialization.json.JsonObject 15 | import kotlinx.serialization.json.jsonObject 16 | 17 | object ExamModel: ExamStorage by getExamStorage() { 18 | 19 | /** 20 | * 查询考试 21 | * @param xnm 学年代码 20xx 22 | * @param xqm 学期代码 12 | 3 23 | */ 24 | suspend fun queryExam(account: EasAccount, term: Int, xnm: String, xqm: String): List { 25 | val json = account.post(QustApi.GET_EXAM) { 26 | setBody(FormDataContent(parameters { 27 | append("xnm", xnm) 28 | append("xqm", xqm) 29 | append("queryModel.showCount", "999") 30 | })) 31 | } 32 | 33 | val item = JsonUtils.parseString(json)["items", JSONArray] ?: return emptyList() 34 | 35 | return List(item.size) { 36 | val js = item[it].jsonObject 37 | Exam( 38 | term = term, 39 | name = js["kcmc", ""], 40 | place = js["kssj", ""], 41 | time = js["cdmc", ""], 42 | ) 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/room/dao/AcademicMapper.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.room.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | import com.qust.helper.room.entity.AcademicGroupDao 7 | import com.qust.helper.room.entity.AcademicInfoDao 8 | 9 | @Dao 10 | interface AcademicMapper { 11 | 12 | @Query("SELECT * FROM academic_info") 13 | fun selectInfo(): List 14 | 15 | @Query("SELECT * FROM academic_group") 16 | fun selectGroups(): List 17 | 18 | @Query("SELECT * FROM academic_info WHERE `index` = :term") 19 | fun selectByTerm(term: Int): List 20 | 21 | @Query("SELECT * FROM academic_info WHERE `group` = :group") 22 | fun selectByGroup(group: Int): List 23 | 24 | @Query("""SELECT 25 | `index` AS 'group', 26 | '' AS type, 27 | COUNT(1) AS totalCounts, 28 | SUM(credit) AS requireCredits, 29 | SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) AS passedCounts, 30 | SUM(CASE WHEN status = 4 THEN credit ELSE 0 END) AS obtainedCredits, 31 | SUM(CASE WHEN status != 4 THEN credit ELSE 0 END) AS creditNotEarned 32 | FROM academic_info GROUP BY `index` 33 | """) 34 | fun selectGroupByTerm(): List 35 | 36 | 37 | @Insert 38 | fun insertInfo(lessons: List) 39 | 40 | @Insert 41 | fun insertGroups(lessons: List) 42 | 43 | @Query("DELETE FROM academic_info") 44 | fun clearInfo() 45 | 46 | @Query("DELETE FROM academic_group") 47 | fun clearGroups() 48 | } 49 | 50 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/ui/Prev.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.ui.tooling.preview.Preview 7 | import com.qust.helper.data.i18n.Strings 8 | import com.qust.helper.entity.lesson.Lesson 9 | import com.qust.helper.ui.widget.lesson.lessonEdit.LessonWeekPicker 10 | import com.qust.helper.ui.widget.lesson.lessonTable.LessonTableUI 11 | import com.qust.helper.ui.widget.lesson.lessonTable.LessonTableUIState 12 | 13 | //@Preview(backgroundColor = 0xFFE6E6E6, showSystemUi = false) 14 | @Composable 15 | fun TestLessonTable() { 16 | val uiState = remember { 17 | val ui = LessonTableUIState() 18 | ui.setLessonTable( 19 | listOf( 20 | Lesson( 21 | colorLabel = 1, 22 | week = 1, 23 | startMinute = 480, 24 | endMinute = 600, 25 | name = "高数1", 26 | place = "明德", 27 | teacher = "老师" 28 | ), 29 | 30 | Lesson( 31 | colorLabel = 1, 32 | week = 2, 33 | startMinute = 500, 34 | endMinute = 640, 35 | name = "高数2", 36 | place = "明德", 37 | teacher = "老师" 38 | ), 39 | ) 40 | ) 41 | 42 | ui 43 | } 44 | LessonTableUI(uiState) 45 | } 46 | 47 | 48 | @Preview(showBackground = true, showSystemUi = true) 49 | @Composable 50 | fun TestLessonTime(){ 51 | val s = remember { mutableStateOf(0) } 52 | val e = remember { mutableStateOf("") } 53 | 54 | LessonWeekPicker("上课时间", s, Strings.ARRAY_WEEK_NAME) 55 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/account/EasLoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel.account 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import com.qust.helper.data.Keys 7 | import com.qust.helper.model.account.EasAccount 8 | import com.qust.helper.utils.DateUtils 9 | import com.qust.helper.utils.SettingUtils 10 | import com.qust.helper.viewmodel.RequestViewModel 11 | import com.qust.helper.viewmodel.extend.toastError 12 | import com.qust.helper.viewmodel.extend.toastOK 13 | 14 | class EasLoginViewModel : RequestViewModel() { 15 | 16 | val account = mutableStateOf(SettingUtils[Keys.EAS_ACCOUNT, ""]) 17 | val password = mutableStateOf(SettingUtils[Keys.EAS_PASSWORD, ""]) 18 | 19 | var accountError by mutableStateOf("") 20 | var passwordError by mutableStateOf("") 21 | 22 | fun login(onLogin: () -> Unit){ 23 | if(account.value.isEmpty()) { accountError = "请输入学号"; return } 24 | if(account.value.length < 2){ accountError = "学号格式错误"; return } 25 | if(password.value.isEmpty()) { passwordError = "请输入密码"; return } 26 | 27 | accountError = "" 28 | passwordError = "" 29 | 30 | request({ 31 | val result = EasAccount.login(account.value, password.value, true) 32 | if(result){ 33 | toastOK("登录成功") 34 | val year = DateUtils.today().year.toString() 35 | val currentYear = (year.substring(0, year.length - 2) + account.value.substring(0, 2)).toInt() 36 | EasAccount.entranceDate = currentYear 37 | onLogin() 38 | }else{ 39 | toastError("用户名或密码错误") 40 | } 41 | 42 | }) 43 | } 44 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/model/eas/NoticeModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.eas 2 | 3 | import com.qust.helper.data.QustApi 4 | import com.qust.helper.entity.eas.Notice 5 | import com.qust.helper.model.account.EasAccount 6 | import com.qust.helper.utils.JSONArray 7 | import com.qust.helper.utils.JsonUtils 8 | import com.qust.helper.utils.JsonUtils.get 9 | import com.qust.helper.utils.Logger 10 | import io.ktor.client.request.forms.FormDataContent 11 | import io.ktor.client.request.setBody 12 | import io.ktor.http.parameters 13 | import kotlinx.serialization.json.JsonObject 14 | import kotlinx.serialization.json.jsonObject 15 | 16 | object NoticeModel { 17 | 18 | /** 19 | * 查询教务通知 20 | * @param page 第几页 21 | * @param pageSize 每页数量 22 | */ 23 | suspend fun queryNotice(account: EasAccount, page: Int = 1, pageSize: Int = 1): List { 24 | try{ 25 | val body = account.post(QustApi.EA_SYSTEM_NOTICE){ 26 | setBody(FormDataContent(parameters { 27 | append("queryModel.showCount", pageSize.toString()) 28 | append("queryModel.currentPage", page.toString()) 29 | append("queryModel.sortName", "cjsj") 30 | append("queryModel.sortOrder", "desc") 31 | })) 32 | } 33 | if(body.startsWith("{") || body.startsWith("[")) { 34 | val item = JsonUtils.parseString(body)["items", JSONArray] ?: return emptyList() 35 | return List(item.size){ Notice.createFromJson(item[it].jsonObject) } 36 | }else{ 37 | return emptyList() 38 | } 39 | }catch(e: Exception){ 40 | Logger.e("'url:'${QustApi.EA_SYSTEM_NOTICE}", e) 41 | return emptyList() 42 | } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/utils/JsonUtils.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | import kotlinx.serialization.json.Json 4 | import kotlinx.serialization.json.JsonArray 5 | import kotlinx.serialization.json.JsonObject 6 | import kotlinx.serialization.json.JsonPrimitive 7 | import kotlinx.serialization.json.boolean 8 | import kotlinx.serialization.json.double 9 | import kotlinx.serialization.json.float 10 | import kotlinx.serialization.json.int 11 | import kotlinx.serialization.json.long 12 | 13 | object JSONObject 14 | object JSONArray 15 | object JSONParam 16 | 17 | object JsonUtils { 18 | 19 | val json = Json { ignoreUnknownKeys = true } 20 | 21 | fun parseString(string: String) = json.parseToJsonElement(string) as T 22 | 23 | operator fun JsonObject.get(key: String, def: Int) = (this[key] as? JsonPrimitive)?.int ?: def 24 | operator fun JsonObject.get(key: String, def: Long) = (this[key] as? JsonPrimitive)?.long ?: def 25 | operator fun JsonObject.get(key: String, def: Float) = (this[key] as? JsonPrimitive)?.float ?: def 26 | operator fun JsonObject.get(key: String, def: String) = (this[key] as? JsonPrimitive)?.content ?: def 27 | operator fun JsonObject.get(key: String, def: Double) = (this[key] as? JsonPrimitive)?.double ?: def 28 | operator fun JsonObject.get(key: String, def: Boolean) = (this[key] as? JsonPrimitive)?.boolean ?: def 29 | 30 | operator fun JsonObject.get(key: String, type: JSONObject) = (this[key] as? JsonObject) 31 | operator fun JsonObject.get(key: String, type: JSONArray) = (this[key] as? JsonArray) 32 | operator fun JsonObject.get(key: String, type: JSONParam) = (this[key] as? JsonPrimitive) 33 | } 34 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/model/network/Network.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.network 2 | 3 | import com.qust.helper.utils.SettingUtils 4 | import io.ktor.client.HttpClient 5 | import io.ktor.client.HttpClientConfig 6 | import io.ktor.client.plugins.cookies.CookiesStorage 7 | import io.ktor.http.Cookie 8 | import io.ktor.http.Url 9 | import kotlinx.coroutines.sync.Mutex 10 | import kotlinx.coroutines.sync.withLock 11 | 12 | expect fun httpClient(config: HttpClientConfig<*>.() -> Unit): HttpClient 13 | 14 | /** 15 | * Cookie 持久化类 16 | */ 17 | class AppCookiesStorage( 18 | private val cookieName: String 19 | ): CookiesStorage { 20 | 21 | private val mutex = Mutex() 22 | private val cookies = mutableListOf() 23 | 24 | init { 25 | SettingUtils.getStringSet(cookieName).forEach { cookieData -> 26 | val sp = cookieData.indexOfFirst { it == '=' } 27 | if(sp != -1 && sp < cookieData.length - 1){ 28 | cookies.add(Cookie(cookieData.substring(0, sp), cookieData.substring(sp + 1))) 29 | } 30 | } 31 | } 32 | 33 | override suspend fun get(requestUrl: Url): List = mutex.withLock { 34 | return cookies 35 | } 36 | 37 | override suspend fun addCookie(requestUrl: Url, cookie: Cookie) { 38 | if (cookie.name.isBlank() || cookie.maxAge == 0) return 39 | 40 | mutex.withLock { 41 | cookies.removeAll { it.name == cookie.name } 42 | cookies.add(Cookie(cookie.name, cookie.value)) 43 | SettingUtils.putStringSet(cookieName, cookies.map { "${it.name}=${it.value}" }.toSet()) 44 | } 45 | } 46 | 47 | suspend fun clear() = mutex.withLock { 48 | cookies.clear() 49 | } 50 | 51 | override fun close() { } 52 | } 53 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/tips_warning.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 22 | 27 | 32 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/tips_warning.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 22 | 27 | 32 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/model/lessonTable/LessonTableModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.lessonTable 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableIntStateOf 5 | import androidx.compose.runtime.mutableStateOf 6 | import com.qust.helper.entity.lesson.TimeTable 7 | import com.qust.helper.model.SettingModel 8 | import com.qust.helper.model.database.LessonTableStorage 9 | import com.qust.helper.model.database.getLessonTableStorage 10 | import com.qust.helper.model.eas.LessonTableQueryResult 11 | import com.qust.helper.utils.LessonUtils 12 | 13 | object LessonTableModel: LessonTableStorage by getLessonTableStorage() { 14 | 15 | /** 时间表 */ 16 | val _timeTable = mutableStateOf(TimeTable.DEFAULT) 17 | val timeTable by _timeTable 18 | 19 | /** 开学时间 */ 20 | val _startDay = mutableStateOf(SettingModel.startDay) 21 | val startDay by _startDay 22 | 23 | /** 总周数 */ 24 | val _totalWeek = mutableIntStateOf(SettingModel.totalWeek) 25 | val totalWeek by _totalWeek 26 | 27 | /** 当前周 (从 0 开始) */ 28 | var _currentWeek = mutableIntStateOf(0) 29 | val currentWeek by _currentWeek 30 | 31 | /** 当前星期 ( 0 - 6, 周一 —— 周日) */ 32 | var _dayOfWeek = mutableIntStateOf(0) 33 | val dayOfWeek by _dayOfWeek 34 | 35 | 36 | suspend fun saveLessonTable(result: LessonTableQueryResult){ 37 | val lessons = result.lessons ?: return 38 | 39 | val lessonChange = LessonUtils.mergeLesson(lessons, getAllLesson()) 40 | 41 | mergeLesson(lessonChange.first, lessonChange.second, lessonChange.third) 42 | 43 | _startDay.value = result.startDay 44 | _totalWeek.value = result.totalWeek 45 | 46 | SettingModel.startDay = result.startDay 47 | SettingModel.totalWeek = result.totalWeek 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/tips_finish.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | 30 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/data/Pages.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.data 2 | 3 | import com.qust.helper.ui.page.BasePage 4 | import com.qust.helper.ui.page.EmptyPage 5 | import com.qust.helper.ui.page.HomePage 6 | import com.qust.helper.ui.page.MyPage 7 | import com.qust.helper.ui.page.account.AccountManagerPage 8 | import com.qust.helper.ui.page.account.EasLoginPage 9 | import com.qust.helper.ui.page.account.IpassLoginPage 10 | import com.qust.helper.ui.page.app.SettingPage 11 | import com.qust.helper.ui.page.eas.QueryAcademicPage 12 | import com.qust.helper.ui.page.eas.QueryExamPage 13 | import com.qust.helper.ui.page.eas.QueryLessonPage 14 | import com.qust.helper.ui.page.eas.QueryMarkPage 15 | import com.qust.helper.ui.page.eas.QueryNoticePage 16 | import com.qust.helper.ui.page.lesson.LessonTablePage 17 | import com.qust.helper.ui.page.third.DrinkPage 18 | 19 | object Pages { 20 | 21 | val Pages = mapOf( 22 | EmptyPage.key to EmptyPage, 23 | 24 | HomePage.key to HomePage, 25 | 26 | LessonTablePage.key to LessonTablePage, 27 | 28 | AccountManagerPage.key to AccountManagerPage, 29 | EasLoginPage.key to EasLoginPage, 30 | IpassLoginPage.key to IpassLoginPage, 31 | 32 | QueryLessonPage.key to QueryLessonPage, 33 | QueryMarkPage.key to QueryMarkPage, 34 | QueryExamPage.key to QueryExamPage, 35 | QueryAcademicPage.key to QueryAcademicPage, 36 | QueryNoticePage.key to QueryNoticePage, 37 | 38 | DrinkPage.key to DrinkPage, 39 | 40 | SettingPage.key to SettingPage 41 | ) 42 | 43 | val defaultPages: List> = listOf(LessonTablePage, MyPage) 44 | 45 | operator fun get(key: String?) = Pages[key] 46 | } 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/tips_finish.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | 30 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/drawables/IconLogin.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.drawables 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val Drawables.IconLogin: ImageVector 10 | get() { 11 | if (_IconLogin != null) { 12 | return _IconLogin!! 13 | } 14 | _IconLogin = ImageVector.Builder( 15 | name = "Login", 16 | defaultWidth = 24.dp, 17 | defaultHeight = 24.dp, 18 | viewportWidth = 24f, 19 | viewportHeight = 24f 20 | ).apply { 21 | path(fill = SolidColor(Color(0xFF000000))) { 22 | moveTo(12f, 12f) 23 | curveToRelative(2.21f, 0f, 4f, -1.79f, 4f, -4f) 24 | reflectiveCurveToRelative(-1.79f, -4f, -4f, -4f) 25 | reflectiveCurveToRelative(-4f, 1.79f, -4f, 4f) 26 | reflectiveCurveToRelative(1.79f, 4f, 4f, 4f) 27 | close() 28 | moveTo(12f, 14f) 29 | curveToRelative(-2.67f, 0f, -8f, 1.34f, -8f, 4f) 30 | verticalLineToRelative(1f) 31 | curveToRelative(0f, 0.55f, 0.45f, 1f, 1f, 1f) 32 | horizontalLineToRelative(14f) 33 | curveToRelative(0.55f, 0f, 1f, -0.45f, 1f, -1f) 34 | verticalLineToRelative(-1f) 35 | curveToRelative(0f, -2.66f, -5.33f, -4f, -8f, -4f) 36 | close() 37 | } 38 | }.build() 39 | 40 | return _IconLogin!! 41 | } 42 | 43 | private var _IconLogin: ImageVector? = null 44 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/room/entity/LessonDao.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.room.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.qust.helper.entity.lesson.Lesson 7 | 8 | @Entity(tableName = "lesson") 9 | class LessonDao( 10 | 11 | @PrimaryKey(autoGenerate = true) 12 | val id: Long = 0L, 13 | 14 | @ColumnInfo val type: Int = 0, 15 | @ColumnInfo val reference: Long = 0L, 16 | 17 | @ColumnInfo val lessonId: String = "", 18 | 19 | @ColumnInfo val colorLabel: Int = 0, 20 | 21 | @ColumnInfo val weeks: Long = 0L, 22 | @ColumnInfo val week: Int = 0, 23 | @ColumnInfo val startMinute: Int = 0, 24 | @ColumnInfo val endMinute: Int = 0, 25 | 26 | @ColumnInfo val name: String = "", 27 | @ColumnInfo val place: String = "", 28 | @ColumnInfo val teacher: String = "", 29 | 30 | @ColumnInfo val remark: String = "" 31 | ) 32 | 33 | fun LessonDao.toLesson(): Lesson = Lesson( 34 | id = this.id, 35 | type = this.type, 36 | reference = this.reference, 37 | lessonId = this.lessonId, 38 | colorLabel = this.colorLabel, 39 | weeks = this.weeks, 40 | week = this.week, 41 | startMinute = this.startMinute, 42 | endMinute = this.endMinute, 43 | name = this.name, 44 | place = this.place, 45 | teacher = this.teacher, 46 | remark = this.remark, 47 | ) 48 | 49 | fun Lesson.toLessonDao(): LessonDao = LessonDao( 50 | id = this.id, 51 | type = this.type, 52 | reference = this.reference, 53 | lessonId = this.lessonId, 54 | colorLabel = this.colorLabel, 55 | weeks = this.weeks, 56 | week = this.week, 57 | startMinute = this.startMinute, 58 | endMinute = this.endMinute, 59 | name = this.name, 60 | place = this.place, 61 | teacher = this.teacher, 62 | remark = this.remark, 63 | ) 64 | 65 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/drawables/Login.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.drawables 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val Drawables.Login: ImageVector 10 | get() { 11 | if (_Login != null) { 12 | return _Login!! 13 | } 14 | _Login = ImageVector.Builder( 15 | name = "Login", 16 | defaultWidth = 24.dp, 17 | defaultHeight = 24.dp, 18 | viewportWidth = 24f, 19 | viewportHeight = 24f 20 | ).apply { 21 | path(fill = SolidColor(Color(0xFF000000))) { 22 | moveTo(12f, 12f) 23 | curveToRelative(2.21f, 0f, 4f, -1.79f, 4f, -4f) 24 | reflectiveCurveToRelative(-1.79f, -4f, -4f, -4f) 25 | reflectiveCurveToRelative(-4f, 1.79f, -4f, 4f) 26 | reflectiveCurveToRelative(1.79f, 4f, 4f, 4f) 27 | close() 28 | moveTo(12f, 14f) 29 | curveToRelative(-2.67f, 0f, -8f, 1.34f, -8f, 4f) 30 | verticalLineToRelative(1f) 31 | curveToRelative(0f, 0.55f, 0.45f, 1f, 1f, 1f) 32 | horizontalLineToRelative(14f) 33 | curveToRelative(0.55f, 0f, 1f, -0.45f, 1f, -1f) 34 | verticalLineToRelative(-1f) 35 | curveToRelative(0f, -2.66f, -5.33f, -4f, -8f, -4f) 36 | close() 37 | } 38 | }.build() 39 | 40 | return _Login!! 41 | } 42 | 43 | @Suppress("ObjectPropertyName") 44 | private var _Login: ImageVector? = null 45 | -------------------------------------------------------------------------------- /iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | CADisableMinimumFrameDurationOnPhone 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/drawables/Water.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.drawables 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val Drawables.Water: ImageVector 10 | get() { 11 | if (_Water != null) { 12 | return _Water!! 13 | } 14 | _Water = ImageVector.Builder( 15 | name = "Water", 16 | defaultWidth = 24.dp, 17 | defaultHeight = 24.dp, 18 | viewportWidth = 24f, 19 | viewportHeight = 24f 20 | ).apply { 21 | path(fill = SolidColor(Color(0xFF000000))) { 22 | moveTo(12f, 2f) 23 | curveToRelative(-5.33f, 4.55f, -8f, 8.48f, -8f, 11.8f) 24 | curveToRelative(0f, 4.98f, 3.8f, 8.2f, 8f, 8.2f) 25 | reflectiveCurveToRelative(8f, -3.22f, 8f, -8.2f) 26 | curveTo(20f, 10.48f, 17.33f, 6.55f, 12f, 2f) 27 | close() 28 | moveTo(7.83f, 14f) 29 | curveToRelative(0.37f, 0f, 0.67f, 0.26f, 0.74f, 0.62f) 30 | curveToRelative(0.41f, 2.22f, 2.28f, 2.98f, 3.64f, 2.87f) 31 | curveToRelative(0.43f, -0.02f, 0.79f, 0.32f, 0.79f, 0.75f) 32 | curveToRelative(0f, 0.4f, -0.32f, 0.73f, -0.72f, 0.75f) 33 | curveToRelative(-2.13f, 0.13f, -4.62f, -1.09f, -5.19f, -4.12f) 34 | curveTo(7.01f, 14.42f, 7.37f, 14f, 7.83f, 14f) 35 | close() 36 | } 37 | }.build() 38 | 39 | return _Water!! 40 | } 41 | 42 | @Suppress("ObjectPropertyName") 43 | private var _Water: ImageVector? = null 44 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/third/DrinkViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel.third 2 | 3 | 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import com.qust.helper.data.Keys 8 | import com.qust.helper.model.account.DrinkAccount 9 | import com.qust.helper.model.network.NeedLoginException 10 | import com.qust.helper.utils.SettingUtils 11 | import com.qust.helper.viewmodel.RequestViewModel 12 | import com.qust.helper.viewmodel.extend.toastError 13 | import com.qust.helper.viewmodel.extend.toastWarning 14 | 15 | class DrinkViewModel : RequestViewModel(), DrinkUIEvent { 16 | 17 | private val drinkAccount = DrinkAccount 18 | 19 | val uiState = DrinkUIState(drinkAccount = drinkAccount) 20 | 21 | override fun login() { 22 | request({ 23 | try { 24 | val result = drinkAccount.login(uiState.account.value, uiState.password.value, true) 25 | if(result) drinkAccount.getDrinkCode() 26 | else toastError("用户名或密码错误") 27 | } catch(e: Exception) { 28 | toastError("网络错误: ${e.message}") 29 | } 30 | }) 31 | } 32 | 33 | override fun getDrinkCode() { 34 | request({ 35 | try { 36 | drinkAccount.getDrinkCode() 37 | } catch(e: NeedLoginException) { 38 | toastWarning("请先登录") 39 | uiState.needLogin = true 40 | } catch(e: Exception) { 41 | toastError("刷新失败: ${e.message}") 42 | } 43 | }) 44 | } 45 | } 46 | 47 | class DrinkUIState(drinkAccount: DrinkAccount) { 48 | var drinkCode by drinkAccount._drinkCode 49 | var account = mutableStateOf(SettingUtils.getString(key = Keys.DRINK_ACCOUNT)) 50 | var password = mutableStateOf(SettingUtils.getString(key = Keys.DRINK_PASSWORD)) 51 | var needLogin by mutableStateOf(false) 52 | } 53 | 54 | interface DrinkUIEvent { 55 | fun login() {} 56 | fun getDrinkCode() {} 57 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/data/Keys.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.data 2 | 3 | object Keys { 4 | 5 | /** 6 | * 是否是第一次使用 7 | */ 8 | const val IS_FIRST_USE = "isFirstUse" 9 | 10 | /** 11 | * 教务节点 12 | */ 13 | const val EA_HOST = "eaHost" 14 | 15 | /** 16 | * 使用VPN访问教务 17 | */ 18 | const val EA_USE_VPN = "eaUseVpn" 19 | 20 | /** 21 | * 入学时间 22 | */ 23 | const val ENTRANCE_TIME = "key_entrance_time" 24 | 25 | /** 26 | * 显示所有课程 27 | */ 28 | const val KEY_SHOW_ALL_LESSON = "key_show_all_lesson" 29 | 30 | /** 31 | * 隐藏已结课课程 32 | */ 33 | const val KEY_HIDE_FINISH_LESSON = "key_hide_finish_lesson" 34 | 35 | /** 36 | * 隐藏教师 37 | */ 38 | const val KEY_HIDE_TEACHER = "key_hide_teacher" 39 | 40 | /** 41 | * 课表时间表 0:冬季, 1:夏季 42 | */ 43 | const val KEY_TIME_TABLE = "key_time_table" 44 | 45 | /** 46 | * 高密时间表 47 | */ 48 | const val KEY_GAOMI_TIME_TABLE = "key_gaomi_time_table" 49 | 50 | /** 51 | * 锁定课表 52 | */ 53 | const val KEY_LOCK_LESSON = "key_lock_lesson" 54 | 55 | 56 | /** 57 | * 主题跟随系统 58 | */ 59 | const val KEY_THEME_FOLLOW_SYSTEM = "key_theme_follow_system" 60 | /** 61 | * 暗色模式 62 | */ 63 | const val KEY_THEME_DARK = "key_theme_dark" 64 | 65 | 66 | /** 67 | * 自动检查更新 68 | */ 69 | const val KEY_AUTO_UPDATE = "key_auto_update" 70 | 71 | /** 72 | * 上次检查更新的时间 73 | */ 74 | const val LAST_UPDATE_TIME = "last_update_time" 75 | 76 | /** 77 | * 上一个教务公告的ID 78 | */ 79 | const val LAST_NOTICE_ID = "last_notice_id" 80 | 81 | 82 | /** 课表设置 */ 83 | const val SETTING_TOTAL_WEEK = "setting_total_week" 84 | const val SETTING_START_DAY = "setting_start_day" 85 | 86 | 87 | 88 | const val EAS_ACCOUNT = "eas_account" 89 | const val EAS_PASSWORD = "eas_password" 90 | 91 | const val IPASS_ACCOUNT = "ipass_account" 92 | const val IPASS_PASSWORD = "ipass_password" 93 | 94 | const val DRINK_ACCOUNT = "drink_account" 95 | const val DRINK_PASSWORD = "drink_password" 96 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/drawables/Visibility.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.drawables 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val Drawables.Visibility: ImageVector 10 | get() { 11 | if (_Visibility != null) { 12 | return _Visibility!! 13 | } 14 | _Visibility = ImageVector.Builder( 15 | name = "Visibility", 16 | defaultWidth = 24.dp, 17 | defaultHeight = 24.dp, 18 | viewportWidth = 24f, 19 | viewportHeight = 24f 20 | ).apply { 21 | path(fill = SolidColor(Color(0xFF000000))) { 22 | moveTo(12f, 4f) 23 | curveTo(7f, 4f, 2.73f, 7.11f, 1f, 11.5f) 24 | curveTo(2.73f, 15.89f, 7f, 19f, 12f, 19f) 25 | reflectiveCurveToRelative(9.27f, -3.11f, 11f, -7.5f) 26 | curveTo(21.27f, 7.11f, 17f, 4f, 12f, 4f) 27 | close() 28 | moveTo(12f, 16.5f) 29 | curveToRelative(-2.76f, 0f, -5f, -2.24f, -5f, -5f) 30 | reflectiveCurveToRelative(2.24f, -5f, 5f, -5f) 31 | reflectiveCurveToRelative(5f, 2.24f, 5f, 5f) 32 | reflectiveCurveToRelative(-2.24f, 5f, -5f, 5f) 33 | close() 34 | moveTo(12f, 8.5f) 35 | curveToRelative(-1.66f, 0f, -3f, 1.34f, -3f, 3f) 36 | reflectiveCurveToRelative(1.34f, 3f, 3f, 3f) 37 | reflectiveCurveToRelative(3f, -1.34f, 3f, -3f) 38 | reflectiveCurveToRelative(-1.34f, -3f, -3f, -3f) 39 | close() 40 | } 41 | }.build() 42 | 43 | return _Visibility!! 44 | } 45 | 46 | @Suppress("ObjectPropertyName") 47 | private var _Visibility: ImageVector? = null 48 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/widget/components/Loading.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.widget.components 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.geometry.CornerRadius 10 | import androidx.compose.ui.geometry.Offset 11 | import androidx.compose.ui.geometry.Size 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.unit.Dp 14 | import androidx.compose.ui.unit.dp 15 | import com.qust.helper.ui.theme.colorSuccess 16 | import com.qust.helper.ui.widget.IndeterminateProgressDialog 17 | import com.qust.helper.viewmodel.extend.LoadingAble 18 | 19 | @Composable 20 | fun LoadingUI(loading: LoadingAble){ 21 | if(loading.loadingText.isNotEmpty()) IndeterminateProgressDialog(loading.loadingText) 22 | } 23 | 24 | 25 | @Composable 26 | fun TripleProgressBar( 27 | redValue: Float = 0F, 28 | greenValue: Float = 0F, 29 | height: Dp = 5.dp, 30 | redColor: Color = MaterialTheme.colorScheme.error, 31 | greenColor: Color = colorSuccess, 32 | ) { 33 | Canvas(modifier = Modifier.fillMaxWidth().height(height)) { 34 | drawRoundRect( 35 | color = Color.Gray, 36 | topLeft = Offset(0F, 0F), 37 | size = Size(size.width, size.height), 38 | cornerRadius = CornerRadius(size.height / 2F), 39 | ) 40 | if(redValue > 0F) { 41 | drawRoundRect( 42 | color = redColor, 43 | topLeft = Offset(0F, 0F), 44 | size = Size(size.width * redValue, size.height), 45 | cornerRadius = CornerRadius(size.height / 2F), 46 | ) 47 | } 48 | if(greenValue > 0F) { 49 | drawRoundRect( 50 | color = greenColor, 51 | topLeft = Offset(0F, 0F), 52 | size = Size(size.width * greenValue, size.height), 53 | cornerRadius = CornerRadius(size.height / 2F), 54 | ) 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/tips_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 23 | 36 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/tips_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 23 | 36 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/page/lesson/LessonTablePage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.page.lesson 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.Button 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | import androidx.lifecycle.viewmodel.compose.viewModel 13 | import com.qust.helper.ui.drawables.Drawables 14 | import com.qust.helper.ui.drawables.GridView 15 | import com.qust.helper.ui.page.BackHandler 16 | import com.qust.helper.ui.page.BasePage 17 | import com.qust.helper.ui.widget.dialog.BottomDialog 18 | import com.qust.helper.ui.widget.lesson.lessonEdit.LessonEditUI 19 | import com.qust.helper.ui.widget.lesson.lessonTable.LessonTableUI 20 | import com.qust.helper.viewmodel.lesson.LessonTableViewModel 21 | 22 | object LessonTablePage: BasePage("学期课表", Drawables.GridView) { 23 | 24 | @Composable 25 | override fun getViewModel() = viewModel() 26 | 27 | @Composable 28 | override fun Content(viewModel: LessonTableViewModel) { 29 | BackHandler(viewModel.isEditLesson) { 30 | viewModel.isEditLesson = false 31 | } 32 | 33 | Box(Modifier.fillMaxSize()) { 34 | 35 | LessonTableUI(viewModel.tableUIState){ i, it -> 36 | viewModel.clickLesson(i, it) 37 | } 38 | 39 | Button(modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp), onClick = { 40 | viewModel.clickLesson(-1, null) 41 | }){ 42 | Text("添加课程") 43 | } 44 | } 45 | 46 | LessonEditDialog(viewModel) 47 | } 48 | 49 | 50 | @Composable 51 | fun LessonEditDialog(viewModel: LessonTableViewModel) { 52 | BottomDialog(isExpanded = viewModel.isEditLesson) { 53 | LessonEditUI( 54 | uiState = viewModel.editUIState, 55 | uiEvent = viewModel, 56 | onDismiss = { viewModel.cancel() }, 57 | ) 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/page/eas/EasCompose.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.page.eas 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.RowScope 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.width 13 | import androidx.compose.foundation.layout.wrapContentSize 14 | import androidx.compose.material3.Button 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.unit.dp 20 | import com.qust.helper.data.i18n.Strings 21 | import com.qust.helper.ui.widget.components.ListItemPicker 22 | 23 | 24 | @Composable 25 | fun BaseEasQueryUI( 26 | pickYear: Int = 0, 27 | onYearPick: (Int) -> Unit = { }, 28 | doQuery: () -> Unit = { }, 29 | searchBar: @Composable RowScope.() -> Unit = { }, 30 | content: @Composable ColumnScope.() -> Unit 31 | ){ 32 | Column(modifier = Modifier.fillMaxSize()) { 33 | Row( 34 | modifier = Modifier.fillMaxWidth(), 35 | horizontalArrangement = Arrangement.Center, 36 | verticalAlignment = Alignment.CenterVertically 37 | ) { 38 | 39 | Text(text = Strings.TEXT_TERM) 40 | 41 | Spacer(Modifier.width(4.dp)) 42 | 43 | ListItemPicker( 44 | value = Strings.ARRAY_TERM_NAME[pickYear], 45 | list = Strings.ARRAY_TERM_NAME, 46 | onValueChange = { i, _ -> onYearPick(i) }, 47 | horizontalPadding = 8.dp 48 | ) 49 | 50 | Spacer(Modifier.width(4.dp)) 51 | 52 | searchBar() 53 | 54 | Button(modifier = Modifier.wrapContentSize().padding(8.dp), onClick = { doQuery() }) { 55 | Text(text = Strings.TEXT_OK, maxLines = 1) 56 | } 57 | } 58 | 59 | content() 60 | } 61 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/utils/LessonUtils.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | import com.qust.helper.entity.lesson.Lesson 4 | 5 | object LessonUtils { 6 | 7 | /** 8 | * 将布尔格式的课程上课时间转换为Long形式 9 | */ 10 | fun getWeeksFromBooleans(booleans: Collection): Long { 11 | var weeks = 0L 12 | var tmp = 1L 13 | for(i in booleans){ 14 | if(i) weeks = weeks or tmp 15 | tmp = tmp shl 1 16 | } 17 | return weeks 18 | } 19 | 20 | 21 | /** 22 | * 合并课程表 23 | * 返还两个列表,分别是 新增的课程,更新的课程,删除课程 24 | * 分开是为了给数据库用,让数据库可以正常更新 25 | */ 26 | fun mergeLesson(old: List, new: List): Triple, List, List> { 27 | val newLessons = mutableListOf() 28 | val updateLessons = mutableListOf() 29 | val deleteLessons = mutableListOf() 30 | 31 | val idMap = mutableMapOf>() 32 | for(lesson in old){ 33 | // 按照 lessonId 将已有课程分组,没有 lessonId 的一般为用户自定义课程 34 | if(lesson.lessonId.isNotEmpty()){ 35 | idMap.getOrPut(lesson.lessonId){ mutableListOf() }.add(lesson) 36 | } 37 | } 38 | 39 | for(lesson in new) { 40 | if(lesson.lessonId.isEmpty()) { 41 | // 没有 lessonId 的直接新增 42 | newLessons.add(lesson) 43 | continue 44 | } 45 | 46 | val list = idMap[lesson.lessonId] 47 | if(list == null){ 48 | // 这个 lessonId 没出现过,是新课程 49 | newLessons.add(lesson) 50 | continue 51 | } 52 | 53 | var index = 0 54 | var find = false 55 | // 从旧课表中 lessonId 相同的课程里找上课时间相同的 56 | while(index < list.size){ 57 | if(list[index].startMinute == lesson.startMinute && list[index].endMinute == lesson.endMinute){ 58 | find = true 59 | updateLessons.add(lesson.copy(id = list[index].id)) // 这里要复制原课程的Id让数据库知道是哪个更新了 60 | list.removeAt(index) // 从列表里删除这个课程 61 | break 62 | } 63 | index++ 64 | } 65 | // 没找到上课时间相符的,说明是新课程 66 | if(!find) newLessons.add(lesson) 67 | 68 | // 旧课表中没有被匹配的课程添加到删除列表 69 | for(lessons in idMap.values){ 70 | deleteLessons.addAll(lessons) 71 | } 72 | } 73 | return Triple(newLessons, updateLessons, deleteLessons) 74 | } 75 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/page/account/EasLoginPage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.page.account 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.Button 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.rememberCoroutineScope 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 12 | import androidx.compose.ui.unit.dp 13 | import androidx.lifecycle.viewmodel.compose.viewModel 14 | import com.qust.helper.data.i18n.Strings 15 | import com.qust.helper.ui.drawables.Drawables 16 | import com.qust.helper.ui.drawables.IconLogin 17 | import com.qust.helper.ui.page.BasePage 18 | import com.qust.helper.ui.page.rememberPageController 19 | import com.qust.helper.ui.widget.form.AccountInput 20 | import com.qust.helper.viewmodel.account.EasLoginViewModel 21 | import kotlinx.coroutines.launch 22 | 23 | object EasLoginPage: BasePage("教务登陆", Drawables.IconLogin) { 24 | 25 | @Composable 26 | override fun getViewModel() = viewModel() 27 | 28 | @Composable 29 | override fun Content(viewModel: EasLoginViewModel) { 30 | val scope = rememberCoroutineScope() 31 | val pageController = rememberPageController() 32 | val keyboardController = LocalSoftwareKeyboardController.current 33 | 34 | Column(Modifier.padding(horizontal = 16.dp)) { 35 | AccountInput( 36 | account = viewModel.account, 37 | password = viewModel.password, 38 | accountError = viewModel.accountError, 39 | passwordError = viewModel.passwordError, 40 | "学号", 41 | "教务系统密码", 42 | login = { 43 | keyboardController?.hide() 44 | viewModel.login { scope.launch { pageController.back() } } 45 | } 46 | ) 47 | Button(modifier = Modifier.fillMaxWidth().padding(8.dp), onClick = { 48 | keyboardController?.hide() 49 | viewModel.login { scope.launch { pageController.back() } } 50 | }){ 51 | Text(text = Strings.TEXT_OK) 52 | } 53 | } 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/utils/SettingUtils.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | import com.qust.helper.platform.utils.SettingUtilsImpl 4 | 5 | expect fun getSettingUtils(): SettingUtilsImpl 6 | 7 | object SettingUtils { 8 | 9 | val impl = getSettingUtils() 10 | 11 | operator fun get(key: String, defValue: T): T { 12 | return when(defValue){ 13 | is String -> (impl.getString(key, defValue) ?: defValue) as T 14 | is Int -> impl.getInt(key, defValue) as T 15 | is Boolean -> impl.getBoolean(key, defValue) as T 16 | is Float -> impl.getFloat(key, defValue) as T 17 | null -> throw IllegalArgumentException("unsupported mmkv value null") 18 | else -> throw IllegalArgumentException("unsupported mmkv value type ${defValue!!::class.simpleName}") 19 | } 20 | } 21 | 22 | operator fun set(key: String, value: T){ 23 | when(value){ 24 | is String -> impl.putString(key, value) 25 | is Int -> impl.putInt(key, value) 26 | is Boolean -> impl.putBoolean(key, value) 27 | is Float -> impl.putFloat(key, value) 28 | null -> throw IllegalArgumentException("unsupported mmkv value null") 29 | else -> throw IllegalArgumentException("unsupported mmkv value type ${value!!::class.simpleName}") 30 | } 31 | } 32 | 33 | fun getString(key: String, defValue: String = "") = impl.getString(key, defValue) ?: defValue 34 | fun putString(key: String, value: String) = impl.putString(key, value) 35 | 36 | fun getInt(key: String, defValue: Int = 0): Int = impl.getInt(key, defValue) 37 | fun putInt(key: String, value: Int) = impl.putInt(key, value) 38 | 39 | fun getLong(key: String, defValue: Long = 0): Long = impl.getLong(key, defValue) 40 | fun putLong(key: String, value: Long) = impl.putLong(key, value) 41 | 42 | fun getBoolean(key: String, defValue: Boolean = false) = impl.getBoolean(key, defValue) 43 | fun putBoolean(key: String, value: Boolean) = impl.putBoolean(key, value) 44 | 45 | fun getStringSet(key: String, defValue: Set = emptySet()) = impl.getStringSet(key, defValue) ?: emptySet() 46 | fun putStringSet(key: String, value: Set) = impl.putStringSet(key, value) 47 | 48 | fun removeKey(key: String) = impl.removeKey(key) 49 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/model/database/AcademicStorage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.database 2 | 3 | import com.qust.helper.entity.eas.AcademicGroup 4 | import com.qust.helper.entity.eas.AcademicInfo 5 | import com.qust.helper.room.AppDataBase 6 | import com.qust.helper.room.entity.AcademicGroupDao 7 | import com.qust.helper.room.entity.AcademicInfoDao 8 | import com.qust.helper.room.entity.toAcademicGroup 9 | import com.qust.helper.room.entity.toAcademicGroupDao 10 | import com.qust.helper.room.entity.toAcademicInfo 11 | import com.qust.helper.room.entity.toAcademicInfoDao 12 | 13 | actual fun getAcademicStorage(): AcademicStorage = AcademicStorageImpl() 14 | 15 | class AcademicStorageImpl: AcademicStorage { 16 | 17 | override suspend fun getInfo(): List { 18 | return AppDataBase.INSTANCE.academicDao().selectInfo().map(AcademicInfoDao::toAcademicInfo) 19 | } 20 | 21 | override suspend fun getInfoByTerm(term: Int): List { 22 | return AppDataBase.INSTANCE.academicDao().selectByTerm(term).map(AcademicInfoDao::toAcademicInfo) 23 | } 24 | 25 | override suspend fun getInfoByGroup(group: Int): List { 26 | return AppDataBase.INSTANCE.academicDao().selectByGroup(group).map(AcademicInfoDao::toAcademicInfo) 27 | } 28 | 29 | override suspend fun getGroups(): List { 30 | return AppDataBase.INSTANCE.academicDao().selectGroups().map(AcademicGroupDao::toAcademicGroup) 31 | } 32 | 33 | override suspend fun getGroupsByTerm(): List { 34 | return AppDataBase.INSTANCE.academicDao().selectGroupByTerm().map(AcademicGroupDao::toAcademicGroup) 35 | } 36 | 37 | override suspend fun insertInfo(info: List) { 38 | AppDataBase.INSTANCE.academicDao().insertInfo(info.map(AcademicInfo::toAcademicInfoDao)) 39 | } 40 | 41 | override suspend fun insertGroups(groups: List) { 42 | AppDataBase.INSTANCE.academicDao().insertGroups(groups.map(AcademicGroup::toAcademicGroupDao)) 43 | } 44 | 45 | override suspend fun clearInfo() { 46 | AppDataBase.INSTANCE.academicDao().clearInfo() 47 | } 48 | 49 | override suspend fun clearGroups() { 50 | AppDataBase.INSTANCE.academicDao().clearGroups() 51 | } 52 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/qust/helper/utils/NotificationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.content.Context 7 | import android.graphics.BitmapFactory 8 | import android.graphics.Color 9 | import android.os.Build 10 | import androidx.core.app.NotificationCompat 11 | import com.qust.helper.R 12 | 13 | object NotificationUtils { 14 | 15 | private const val NOTIFICATION_ID = "push" 16 | private const val NOTIFICATION_NAME = "推送通知" 17 | private const val NOTIFICATION_SHOW_AT_MOST = 10 18 | 19 | private var notificationNum = 0 20 | 21 | fun sendNotification(context: Context, title: String?, content: String) { 22 | if(notificationNum++ > NOTIFICATION_SHOW_AT_MOST) notificationNum = 0 23 | 24 | val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 25 | 26 | // 检查渠道 27 | if(Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { 28 | if(manager.getNotificationChannel(NOTIFICATION_ID) == null) { 29 | val channel = NotificationChannel( 30 | NOTIFICATION_ID, 31 | NOTIFICATION_NAME, 32 | NotificationManager.IMPORTANCE_HIGH 33 | ) 34 | manager.createNotificationChannel(channel) 35 | } 36 | } 37 | 38 | val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, NOTIFICATION_ID) 39 | .setAutoCancel(true) 40 | .setSmallIcon(R.mipmap.ic_launcher) 41 | .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) 42 | .setWhen(System.currentTimeMillis()) 43 | .setDefaults(NotificationCompat.DEFAULT_ALL) 44 | .setPriority(Notification.PRIORITY_MAX) 45 | .setContentTitle(title) 46 | .setContentText(content) 47 | .setColor(Color.TRANSPARENT) 48 | .setCategory(NotificationCompat.CATEGORY_MESSAGE) 49 | .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) 50 | // .addAction(R.mipmap.ic_avatar, "去看看", pendingIntent) 51 | 52 | if(content.length > 20) { 53 | builder.setStyle(NotificationCompat.BigTextStyle().bigText(content)) 54 | } 55 | 56 | manager.notify(notificationNum, builder.build()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/page/account/IpassLoginPage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.page.account 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.Button 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.rememberCoroutineScope 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 12 | import androidx.compose.ui.unit.dp 13 | import androidx.lifecycle.viewmodel.compose.viewModel 14 | import com.qust.helper.data.i18n.Strings 15 | import com.qust.helper.ui.drawables.Drawables 16 | import com.qust.helper.ui.drawables.IconLogin 17 | import com.qust.helper.ui.page.BasePage 18 | import com.qust.helper.ui.page.rememberPageController 19 | import com.qust.helper.ui.widget.form.AccountInput 20 | import com.qust.helper.viewmodel.account.IpassLoginViewModel 21 | import kotlinx.coroutines.launch 22 | 23 | object IpassLoginPage: BasePage("智慧青科大登陆", Drawables.IconLogin) { 24 | 25 | @Composable 26 | override fun getViewModel() = viewModel() 27 | 28 | @Composable 29 | override fun Content(viewModel: IpassLoginViewModel) { 30 | val scope = rememberCoroutineScope() 31 | val pageController = rememberPageController() 32 | val keyboardController = LocalSoftwareKeyboardController.current 33 | 34 | Column(Modifier.padding(horizontal = 16.dp)) { 35 | AccountInput( 36 | account = viewModel.account, 37 | password = viewModel.password, 38 | accountError = viewModel.accountError, 39 | passwordError = viewModel.passwordError, 40 | "学号", 41 | "智慧青科大密码", 42 | login = { 43 | keyboardController?.hide() 44 | viewModel.login { scope.launch { pageController.back() } } 45 | } 46 | ) 47 | Button(modifier = Modifier.fillMaxWidth().padding(8.dp), onClick = { 48 | keyboardController?.hide() 49 | viewModel.login { scope.launch { pageController.back() } } 50 | }){ 51 | Text(text = Strings.TEXT_OK) 52 | } 53 | } 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/eas/QueryExamViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel.eas 2 | 3 | import androidx.compose.runtime.toMutableStateList 4 | import com.qust.helper.data.i18n.Strings 5 | import com.qust.helper.entity.eas.Exam 6 | import com.qust.helper.model.account.EasAccount 7 | import com.qust.helper.model.eas.ExamModel 8 | import com.qust.helper.model.eas.MarkModel 9 | import com.qust.helper.viewmodel.extend.toastError 10 | import com.qust.helper.viewmodel.extend.toastOK 11 | 12 | class QueryExamViewModel: BaseEasViewModel() { 13 | 14 | val exams = emptyList().toMutableStateList() 15 | 16 | private val examData: MutableList?> = MutableList(Strings.ARRAY_TERM_NAME.size) { null } 17 | 18 | fun query(){ 19 | request({ 20 | val term = pickYear.intValue 21 | val picker = getPickTerm() 22 | val resultData = ExamModel.queryExam(EasAccount, term, picker.first, picker.second) 23 | 24 | val origData = examData[term] ?: ExamModel.getExamsByTerm(term) 25 | 26 | if(origData.isEmpty()) { 27 | ExamModel.insertAll(resultData) 28 | examData[term] = null 29 | changeTerm(term) 30 | toastOK("查询完成") 31 | return@request 32 | } 33 | 34 | val origin = origData.toSet() 35 | val result = resultData.toSet() 36 | 37 | val new = result subtract origin 38 | val update = origin intersect result 39 | if(new.isNotEmpty() || update.isNotEmpty()) { 40 | ExamModel.updateAll(update.toList()) 41 | ExamModel.insertAll(new.toList()) 42 | examData[term] = null 43 | changeTerm(term) 44 | toastOK("新查询到 ${new.size} 门考试") 45 | }else{ 46 | toastOK("未查询到新考试") 47 | } 48 | }) { 49 | it.printStackTrace() 50 | toastError("查询失败:${it.message}") 51 | } 52 | } 53 | 54 | fun changeTerm(index: Int){ 55 | val data = examData[index] 56 | if(data == null){ 57 | request({ 58 | val data = ExamModel.getExamsByTerm(index) 59 | examData[index] = data 60 | exams.clear() 61 | exams.addAll(data) 62 | }) 63 | }else{ 64 | exams.clear() 65 | exams.addAll(data) 66 | } 67 | } 68 | 69 | fun clearNew(index: Int) { 70 | runBackGround { 71 | MarkModel.setRead(exams[index].id) 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/layout/widget_lesson.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 24 | 25 | 31 | 32 | 41 | 42 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 44 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/model/eas/MarkModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.eas 2 | 3 | import com.qust.helper.data.QustApi 4 | import com.qust.helper.entity.eas.Mark 5 | import com.qust.helper.model.account.EasAccount 6 | import com.qust.helper.model.database.MarkStorage 7 | import com.qust.helper.model.database.getMarkStorage 8 | import com.qust.helper.utils.JSONArray 9 | import com.qust.helper.utils.JsonUtils 10 | import com.qust.helper.utils.JsonUtils.get 11 | import io.ktor.client.request.forms.FormDataContent 12 | import io.ktor.client.request.setBody 13 | import io.ktor.http.parameters 14 | import kotlinx.serialization.json.JsonObject 15 | import kotlinx.serialization.json.jsonObject 16 | 17 | 18 | object MarkModel: MarkStorage by getMarkStorage() { 19 | 20 | /** 21 | * 查询课表信息 22 | * @param year 学年 23 | * @param term 学期 24 | */ 25 | suspend fun queryMarks(easAccount: EasAccount, term: Int, xnm: String, xqm: String): List { 26 | val markMap = HashMap(32) 27 | 28 | try { 29 | val json = easAccount.post(QustApi.GET_MARK){ 30 | setBody(FormDataContent(parameters { 31 | append("xnm", xnm) 32 | append("xqm", xqm) 33 | append("queryModel.showCount", "999") 34 | })) 35 | } 36 | 37 | JsonUtils.parseString(json)["items", JSONArray]?.forEach { item -> 38 | val js = item.jsonObject 39 | val name = js["kcmc", ""] 40 | if(!markMap.containsKey(name)) { 41 | markMap[name] = Mark.Builder(js) 42 | } 43 | } 44 | } catch(_: Exception) { 45 | return emptyList() 46 | } 47 | 48 | try { 49 | val json = easAccount.post(QustApi.GET_MARK_DETAIL){ 50 | setBody(FormDataContent(parameters { 51 | append("xnm", xnm) 52 | append("xqm", xqm) 53 | append("queryModel.showCount", "999") 54 | })) 55 | } 56 | 57 | JsonUtils.parseString(json)["items", JSONArray]?.forEach { item -> 58 | val js = item.jsonObject 59 | val name = js["kcmc", ""] 60 | var mark = markMap[name] 61 | if(mark == null) { 62 | mark = Mark.Builder(js) 63 | markMap[mark.name] = mark 64 | } 65 | mark.addItemMark(js) 66 | } 67 | } catch(e: Exception) { 68 | e.printStackTrace() 69 | } 70 | return markMap.values.map { it.build(term, true) } 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/eas/QueryLessonViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel.eas 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableIntStateOf 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import com.qust.helper.data.i18n.Strings 8 | import com.qust.helper.model.account.EasAccount 9 | import com.qust.helper.model.eas.LessonQuery 10 | import com.qust.helper.model.eas.LessonTableQueryResult 11 | import com.qust.helper.model.lessonTable.LessonTableModel 12 | import com.qust.helper.ui.widget.lesson.lessonTable.LessonTableUIState 13 | import com.qust.helper.utils.DateUtils 14 | import com.qust.helper.viewmodel.extend.toastError 15 | import com.qust.helper.viewmodel.extend.toastOK 16 | import com.qust.helper.viewmodel.extend.toastWarning 17 | 18 | class QueryLessonViewModel: BaseEasViewModel() { 19 | 20 | val lessonUIState = LessonTableUIState() 21 | 22 | var termText by mutableStateOf("") 23 | var termTimeText by mutableStateOf("") 24 | var totalWeek by mutableIntStateOf(LessonTableModel.totalWeek) 25 | 26 | var needSave by mutableStateOf(false) 27 | 28 | val pickType = mutableIntStateOf(0) 29 | 30 | var queryResult: LessonTableQueryResult? = null 31 | 32 | fun queryLesson(){ 33 | request({ 34 | val pair = getPickTerm() 35 | 36 | val result = LessonQuery.queryLessonTable(easAccount = EasAccount, pair.first, pair.second) 37 | 38 | val error = result.error 39 | if(error != null) { 40 | toastError(error) 41 | return@request 42 | } 43 | 44 | needSave = true 45 | 46 | termText = result.termText 47 | totalWeek = result.totalWeek 48 | 49 | termTimeText = Strings.MSG_QUERY_TERM_START_TIME.format( 50 | DateUtils.YMD.format(LessonTableModel.startDay), 51 | DateUtils.YMD.format(result.startDay) 52 | ) 53 | 54 | val lessons = result.lessons 55 | if(lessons != null) lessonUIState.setLessonTable(lessons) 56 | 57 | queryResult = result 58 | needSave = true 59 | 60 | toastOK("获取课表成功!") 61 | }) 62 | } 63 | 64 | fun saveLesson(){ 65 | val result = queryResult 66 | 67 | if(result == null) { 68 | toastWarning("请先查询课表") 69 | return 70 | } 71 | 72 | request({ 73 | LessonTableModel.saveLessonTable(result) 74 | }, { 75 | toastError("保存课表失败") 76 | }) 77 | } 78 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/drawables/School.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.drawables 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val Drawables.School: ImageVector 10 | get() { 11 | if (_School != null) { 12 | return _School!! 13 | } 14 | _School = ImageVector.Builder( 15 | name = "School", 16 | defaultWidth = 24.dp, 17 | defaultHeight = 24.dp, 18 | viewportWidth = 24f, 19 | viewportHeight = 24f 20 | ).apply { 21 | path(fill = SolidColor(Color(0xFF000000))) { 22 | moveTo(5f, 13.18f) 23 | verticalLineToRelative(2.81f) 24 | curveToRelative(0f, 0.73f, 0.4f, 1.41f, 1.04f, 1.76f) 25 | lineToRelative(5f, 2.73f) 26 | curveToRelative(0.6f, 0.33f, 1.32f, 0.33f, 1.92f, 0f) 27 | lineToRelative(5f, -2.73f) 28 | curveToRelative(0.64f, -0.35f, 1.04f, -1.03f, 1.04f, -1.76f) 29 | verticalLineToRelative(-2.81f) 30 | lineToRelative(-6.04f, 3.3f) 31 | curveToRelative(-0.6f, 0.33f, -1.32f, 0.33f, -1.92f, 0f) 32 | lineTo(5f, 13.18f) 33 | close() 34 | moveTo(11.04f, 3.52f) 35 | lineToRelative(-8.43f, 4.6f) 36 | curveToRelative(-0.69f, 0.38f, -0.69f, 1.38f, 0f, 1.76f) 37 | lineToRelative(8.43f, 4.6f) 38 | curveToRelative(0.6f, 0.33f, 1.32f, 0.33f, 1.92f, 0f) 39 | lineTo(21f, 10.09f) 40 | lineTo(21f, 16f) 41 | curveToRelative(0f, 0.55f, 0.45f, 1f, 1f, 1f) 42 | reflectiveCurveToRelative(1f, -0.45f, 1f, -1f) 43 | lineTo(23f, 9.59f) 44 | curveToRelative(0f, -0.37f, -0.2f, -0.7f, -0.52f, -0.88f) 45 | lineToRelative(-9.52f, -5.19f) 46 | curveToRelative(-0.6f, -0.32f, -1.32f, -0.32f, -1.92f, 0f) 47 | close() 48 | } 49 | }.build() 50 | 51 | return _School!! 52 | } 53 | 54 | @Suppress("ObjectPropertyName") 55 | private var _School: ImageVector? = null 56 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/model/account/DrinkAccount.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.model.account 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import com.qust.helper.data.Keys 5 | import com.qust.helper.utils.JSONObject 6 | import com.qust.helper.utils.JsonUtils 7 | import com.qust.helper.utils.JsonUtils.get 8 | import com.qust.helper.utils.SettingUtils 9 | import io.ktor.client.request.HttpRequestBuilder 10 | import io.ktor.client.request.setBody 11 | import io.ktor.client.statement.HttpResponse 12 | import io.ktor.client.statement.bodyAsText 13 | import io.ktor.http.URLProtocol 14 | import kotlinx.serialization.json.JsonObject 15 | 16 | object DrinkAccount: Account( 17 | protocol = URLProtocol.HTTPS, 18 | host = "dcxy-customer-app.dcrym.com", 19 | accountKey = Keys.DRINK_ACCOUNT, 20 | passwordKey = Keys.DRINK_PASSWORD, 21 | cookieKey = "drinkCookie" 22 | ) { 23 | 24 | private var userToken = SettingUtils.getString("drinkToken") 25 | set(value){ 26 | field = value 27 | SettingUtils["drinkToken"] = value 28 | } 29 | 30 | val _drinkCode = mutableStateOf(SettingUtils.getString("drinkCode")) 31 | fun setDrinkCodeValue(value: String) { 32 | _drinkCode.value = value 33 | SettingUtils["drinkCode"] = value 34 | } 35 | 36 | override suspend fun baseLogin(account: String, password: String): Boolean { 37 | try { 38 | val resp = postOriginal("app/customer/login"){ 39 | headers.append("Content-Type", "application/json;charset=UTF-8") 40 | setBody("""{"loginAccount": "$account", "password": "$password"}""") 41 | }.bodyAsText() 42 | val js = JsonUtils.parseString(resp) 43 | if(js["code", 0] == 1000) { 44 | val data = js["data", JSONObject] ?: return false 45 | userToken = data["token", ""] 46 | return userToken.isNotEmpty() 47 | } else { 48 | return false 49 | } 50 | }catch(e: Exception){ 51 | e.printStackTrace() 52 | return false 53 | } 54 | } 55 | 56 | override suspend fun execute(request: HttpRequestBuilder): HttpResponse { 57 | request.headers.append("clientsource", "{}") 58 | request.headers.append("token", userToken) 59 | return super.execute(request) 60 | } 61 | 62 | suspend fun getDrinkCode(){ 63 | val js = JsonUtils.parseString(get("app/customer/flush/idbar")) 64 | val data: String = js["data", ""] 65 | setDrinkCodeValue(data.substring(0, data.length - 1) + "3") 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/drawables/Notification.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.drawables 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val Drawables.Notification: ImageVector 10 | get() { 11 | if (_Notification != null) { 12 | return _Notification!! 13 | } 14 | _Notification = ImageVector.Builder( 15 | name = "Notification", 16 | defaultWidth = 24.dp, 17 | defaultHeight = 24.dp, 18 | viewportWidth = 24f, 19 | viewportHeight = 24f 20 | ).apply { 21 | path(fill = SolidColor(Color(0xFF000000))) { 22 | moveTo(19.29f, 17.29f) 23 | lineTo(18f, 16f) 24 | verticalLineToRelative(-5f) 25 | curveToRelative(0f, -3.07f, -1.64f, -5.64f, -4.5f, -6.32f) 26 | lineTo(13.5f, 4f) 27 | curveToRelative(0f, -0.83f, -0.67f, -1.5f, -1.5f, -1.5f) 28 | reflectiveCurveToRelative(-1.5f, 0.67f, -1.5f, 1.5f) 29 | verticalLineToRelative(0.68f) 30 | curveTo(7.63f, 5.36f, 6f, 7.92f, 6f, 11f) 31 | verticalLineToRelative(5f) 32 | lineToRelative(-1.29f, 1.29f) 33 | curveToRelative(-0.63f, 0.63f, -0.19f, 1.71f, 0.7f, 1.71f) 34 | horizontalLineToRelative(13.17f) 35 | curveToRelative(0.9f, 0f, 1.34f, -1.08f, 0.71f, -1.71f) 36 | close() 37 | moveTo(16f, 17f) 38 | lineTo(8f, 17f) 39 | verticalLineToRelative(-6f) 40 | curveToRelative(0f, -2.48f, 1.51f, -4.5f, 4f, -4.5f) 41 | reflectiveCurveToRelative(4f, 2.02f, 4f, 4.5f) 42 | verticalLineToRelative(6f) 43 | close() 44 | moveTo(12f, 22f) 45 | curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f) 46 | horizontalLineToRelative(-4f) 47 | curveToRelative(0f, 1.1f, 0.89f, 2f, 2f, 2f) 48 | close() 49 | } 50 | }.build() 51 | 52 | return _Notification!! 53 | } 54 | 55 | @Suppress("ObjectPropertyName") 56 | private var _Notification: ImageVector? = null 57 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.5.2" 3 | 4 | androidx-activityCompose = "1.9.3" 5 | androidx-lifecycle = "2.8.4" 6 | compose-multiplatform = "1.7.0" 7 | 8 | androidxRoom = "2.7.0-alpha01" 9 | 10 | ksp = "2.1.10-1.0.31" 11 | kotlin = "2.1.0" 12 | kotlinx-datetime = "0.6.2" 13 | kotlinx-coroutines = "1.10.1" 14 | 15 | 16 | ktor = "3.0.3" 17 | mmkv = "1.2.15" 18 | sqlite = "2.5.0-SNAPSHOT" 19 | 20 | 21 | [libraries] 22 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 23 | androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } 24 | androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } 25 | androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } 26 | androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidxRoom" } 27 | androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidxRoom" } 28 | 29 | kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime" } 30 | kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } 31 | 32 | ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } 33 | ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } 34 | ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } 35 | 36 | mmkv = { module = "com.tencent:mmkv", version.ref = "mmkv" } 37 | sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } 38 | 39 | 40 | 41 | [plugins] 42 | androidApplication = { id = "com.android.application", version.ref = "agp" } 43 | androidLibrary = { id = "com.android.library", version.ref = "agp" } 44 | composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } 45 | composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 46 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 47 | room = { id = "androidx.room", version.ref = "androidxRoom" } 48 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/widget/dialog/BottomDialog.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.widget.dialog 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.offset 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.setValue 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.layout.onGloballyPositioned 20 | import androidx.compose.ui.platform.LocalDensity 21 | import androidx.compose.ui.unit.Dp 22 | import androidx.compose.ui.unit.dp 23 | 24 | @Composable 25 | fun BottomDialog( 26 | isExpanded: Boolean, 27 | content: @Composable () -> Unit 28 | ) { 29 | val density = LocalDensity.current 30 | var screenHeight by remember { mutableStateOf(0.dp) } 31 | 32 | Box(modifier = Modifier.fillMaxSize().onGloballyPositioned { coordinates -> 33 | with(density) { 34 | screenHeight = coordinates.size.height.toDp() 35 | } 36 | }) { 37 | BottomSlideComponent( 38 | isExpanded = isExpanded, 39 | screenHeight = screenHeight, 40 | content = content 41 | ) 42 | } 43 | } 44 | 45 | @Composable 46 | fun BottomSlideComponent( 47 | isExpanded: Boolean, 48 | screenHeight: Dp, 49 | content: @Composable () -> Unit 50 | ) { 51 | var visibility by remember { mutableStateOf(false) } 52 | 53 | val animationProgress by animateFloatAsState( 54 | targetValue = if(isExpanded){ 55 | visibility = true 56 | 1f 57 | } else 0f, 58 | animationSpec = tween(durationMillis = 500), 59 | label = "slide_animation" 60 | ){ 61 | if(!isExpanded) visibility = false 62 | } 63 | 64 | if(!visibility) return 65 | 66 | val offset = screenHeight - screenHeight * animationProgress 67 | 68 | if(animationProgress < 0.99f) { 69 | Box(modifier = Modifier.fillMaxWidth().background(Color.Black.copy(alpha = 0.3f * (1 - animationProgress)),)) 70 | } 71 | 72 | Box(modifier = Modifier.fillMaxWidth().height(screenHeight).offset(y = offset).background(MaterialTheme.colorScheme.surface)) { 73 | content() 74 | } 75 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/drawables/Article.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.drawables 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val Drawables.Article: ImageVector 10 | get() { 11 | if (_Article != null) { 12 | return _Article!! 13 | } 14 | _Article = ImageVector.Builder( 15 | name = "Article", 16 | defaultWidth = 24.dp, 17 | defaultHeight = 24.dp, 18 | viewportWidth = 24f, 19 | viewportHeight = 24f, 20 | autoMirror = true 21 | ).apply { 22 | path(fill = SolidColor(Color(0xFF000000))) { 23 | moveTo(19f, 3f) 24 | horizontalLineTo(5f) 25 | curveTo(3.9f, 3f, 3f, 3.9f, 3f, 5f) 26 | verticalLineToRelative(14f) 27 | curveToRelative(0f, 1.1f, 0.9f, 2f, 2f, 2f) 28 | horizontalLineToRelative(14f) 29 | curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f) 30 | verticalLineTo(5f) 31 | curveTo(21f, 3.9f, 20.1f, 3f, 19f, 3f) 32 | close() 33 | moveTo(13f, 17f) 34 | horizontalLineTo(8f) 35 | curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f) 36 | curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f) 37 | horizontalLineToRelative(5f) 38 | curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f) 39 | curveTo(14f, 16.55f, 13.55f, 17f, 13f, 17f) 40 | close() 41 | moveTo(16f, 13f) 42 | horizontalLineTo(8f) 43 | curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f) 44 | curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f) 45 | horizontalLineToRelative(8f) 46 | curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f) 47 | curveTo(17f, 12.55f, 16.55f, 13f, 16f, 13f) 48 | close() 49 | moveTo(16f, 9f) 50 | horizontalLineTo(8f) 51 | curveTo(7.45f, 9f, 7f, 8.55f, 7f, 8f) 52 | curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f) 53 | horizontalLineToRelative(8f) 54 | curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f) 55 | curveTo(17f, 8.55f, 16.55f, 9f, 16f, 9f) 56 | close() 57 | } 58 | }.build() 59 | 60 | return _Article!! 61 | } 62 | 63 | @Suppress("ObjectPropertyName") 64 | private var _Article: ImageVector? = null 65 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/entity/eas/Academic.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.entity.eas 2 | 3 | import com.qust.helper.utils.JsonUtils.get 4 | import kotlinx.serialization.json.JsonObject 5 | 6 | /** 7 | * 学业情况查询课程 8 | * 9 | * @param kchId 课程号 10 | * @param name 课程名称 11 | * @param type 课程类型 12 | * 13 | * @param status 修读状态 14 | * 15 | * @param credit 学分 16 | * @param mark 成绩 17 | * @param gpa 绩点 18 | * 19 | * @param category 课程类别名称 20 | * @param content 课程组成 21 | * 22 | * @param index 学年学期索引 23 | */ 24 | data class AcademicInfo( 25 | val id: Int = 0, 26 | 27 | val kchId: String, 28 | val name: String, 29 | val type: String, 30 | 31 | val credit: Float, 32 | val mark: String, 33 | val gpa: Float, 34 | 35 | val status: Int = 0, 36 | 37 | val category: String, 38 | val content: String, 39 | 40 | val index: Int, 41 | val group: Int, 42 | ){ 43 | companion object { 44 | fun createFromJson(js: JsonObject, entranceTime: Int, group: Int): AcademicInfo { 45 | val year: Int = if(js.contains("XNM")) { 46 | js["XNM", ""].toIntOrNull() ?: 0 47 | } else { 48 | js["JYXDXNM", ""].toIntOrNull() ?: 0 49 | } 50 | 51 | val term = if(js.contains("XQMMC")) { 52 | js["XQMMC", ""].toIntOrNull() ?: 0 53 | } else { 54 | js["JYXDXQMC", ""].toIntOrNull() ?: 0 55 | } 56 | 57 | return AcademicInfo( 58 | kchId = js["KCH_ID", ""], 59 | name = js["KCMC", ""].trim(), 60 | type = js["KCXZMC", ""], 61 | 62 | credit = js["XF", ""].toFloatOrNull() ?: 0F, 63 | mark = js["MAXCJ", ""], 64 | gpa = js["JD", 0F], 65 | 66 | status = js["XDZT", ""].toIntOrNull() ?: 0, 67 | 68 | category = js["KCLBMC", ""], 69 | content = js["XSXXXX", ""], 70 | 71 | index = (year - entranceTime) * 2 + term - 1, 72 | group = group 73 | ) 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * @param group 课程分组 80 | * @param type 课程性质名称 81 | * @param requireCredits 要求学分 82 | * @param obtainedCredits 已获得学分 83 | * @param creditNotEarned 未通过学分 84 | * @param passedCounts 已通过门数 85 | * @param totalCounts 总共门数 86 | */ 87 | data class AcademicGroup( 88 | val group: Int, 89 | val type: String = "", 90 | val requireCredits: Float = 0f, 91 | val obtainedCredits: Float = 0f, 92 | val creditNotEarned: Float = 0f, 93 | val passedCounts: Int = 0, 94 | val totalCounts: Int = 0 95 | ){ 96 | class Builder( 97 | var group: Int = 0, 98 | var type: String = "", 99 | var requireCredits: Float = 0f, 100 | var obtainedCredits: Float = 0f, 101 | var creditNotEarned: Float = 0f, 102 | var passedCounts: Int = 0, 103 | var totalCounts: Int = 0, 104 | ) { 105 | fun build() = AcademicGroup(group, type, requireCredits, obtainedCredits, creditNotEarned, passedCounts, totalCounts) 106 | } 107 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/viewmodel/app/SettingViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.viewmodel.app 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import com.qust.helper.model.SettingModel 6 | import com.qust.helper.model.account.EasAccount 7 | import com.qust.helper.model.lessonTable.LessonTableModel 8 | import com.qust.helper.utils.DateUtils 9 | import com.qust.helper.utils.Logger 10 | import com.qust.helper.viewmodel.BaseViewModel 11 | import com.qust.helper.viewmodel.extend.toastWarning 12 | 13 | class SettingViewModel: BaseViewModel() { 14 | 15 | val totalWeek by LessonTableModel._totalWeek 16 | fun setTotalWeek(weekStr: String){ 17 | val week = weekStr.toIntOrNull() 18 | if(week == null){ 19 | toastWarning("请输入正确的数字") 20 | return 21 | } 22 | SettingModel.totalWeek = week 23 | LessonTableModel._totalWeek.value = week 24 | } 25 | 26 | val _startDay = mutableStateOf(DateUtils.YMD.format(LessonTableModel.startDay)) 27 | val startDay by _startDay 28 | fun setStartDay(startDayStr: String){ 29 | try { 30 | val date = DateUtils.YMD.parse(startDayStr) 31 | 32 | _startDay.value = startDayStr 33 | 34 | SettingModel.startDay = date 35 | LessonTableModel._startDay.value = date 36 | } catch(e: Exception) { 37 | Logger.e("", e) 38 | toastWarning("请输入正确的日期") 39 | } 40 | } 41 | 42 | val entranceTime = mutableStateOf(EasAccount.entranceDate.toString()) 43 | fun setEntranceTime(value: Int) { 44 | entranceTime.value = value.toString() 45 | EasAccount.entranceDate = value 46 | } 47 | 48 | // val eaHost by mutableIntStateOf(SettingUtils[Keys.EA_HOST, 0]) 49 | // fun setEaHostValue(value: Int){ 50 | // EASAccount.getInstance().changeHost(value) 51 | // eaHost = value 52 | // } 53 | 54 | // var eaUseVpn by mutableStateOf(SettingUtils.getBoolean(Keys.EA_USE_VPN, false)); private set 55 | // fun setEaUseVpnValue(value: Boolean){ 56 | // EASAccount.useVpn = value 57 | // SettingUtils.putBoolean(Keys.EA_USE_VPN, value) 58 | // eaUseVpn = value 59 | // } 60 | 61 | // var themeFollowSystem by SettingModel.themeFollowSystem; private set 62 | // fun setThemeFollowSystemValue(value: Boolean){ 63 | // SettingUtils.putBoolean(Keys.KEY_THEME_FOLLOW_SYSTEM, value) 64 | // themeFollowSystem = value 65 | // } 66 | // var themeDark by SettingModel.themeDark; private set 67 | // fun setThemeDarkValue(value: Boolean){ 68 | // SettingUtils.putBoolean(Keys.KEY_THEME_DARK, value) 69 | // themeDark = value 70 | // } 71 | 72 | // var autoUpdate by mutableStateOf(SettingUtils.getBoolean(Keys.KEY_AUTO_UPDATE, true)); private set 73 | // fun setAutoUpdateValue(value: Boolean){ 74 | // SettingUtils.putBoolean(Keys.KEY_AUTO_UPDATE, value) 75 | // autoUpdate = value 76 | // } 77 | 78 | // val buildTime by mutableStateOf(BuildConfig.PACKAGE_TIME) 79 | // val appVersion by mutableStateOf(BuildConfig.VERSION_NAME) 80 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/widget/layout/AppLayout.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.widget.layout 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.ColumnScope 7 | import androidx.compose.foundation.layout.PaddingValues 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.heightIn 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 15 | import androidx.compose.material3.Icon 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.draw.clip 22 | import androidx.compose.ui.unit.dp 23 | 24 | @Composable 25 | fun AppContentWithBack( 26 | title: String, 27 | contentPadding: PaddingValues, 28 | onBack: (() -> Unit)? = null, 29 | content: @Composable ColumnScope.() -> Unit 30 | ) { 31 | Column(Modifier.fillMaxSize().padding(contentPadding)) { 32 | Row(modifier = Modifier.heightIn(min = 64.dp), verticalAlignment = Alignment.CenterVertically) { 33 | if(onBack != null){ 34 | Box(modifier = Modifier.padding(start = 8.dp).clip(RoundedCornerShape(32.dp)).clickable(onClick = onBack)){ 35 | Icon( 36 | imageVector = Icons.AutoMirrored.Filled.ArrowBack, 37 | contentDescription = "Back", 38 | modifier = Modifier.padding(8.dp) 39 | ) 40 | } 41 | } 42 | Text( 43 | text = title, 44 | modifier = Modifier.padding(16.dp), 45 | style = MaterialTheme.typography.titleLarge, 46 | maxLines = 1, 47 | ) 48 | } 49 | 50 | Column(Modifier.weight(1F), content = content) 51 | } 52 | } 53 | 54 | @Composable 55 | fun AppContent( 56 | title: String, 57 | contentPadding: PaddingValues, 58 | content: @Composable ColumnScope.() -> Unit 59 | ) { 60 | Column(Modifier.fillMaxSize().padding(contentPadding)) { 61 | Row(modifier = Modifier.heightIn(min = 64.dp), verticalAlignment = Alignment.CenterVertically) { 62 | Text( 63 | text = title, 64 | modifier = Modifier.padding(16.dp), 65 | style = MaterialTheme.typography.titleLarge, 66 | maxLines = 1, 67 | ) 68 | } 69 | Column(Modifier.weight(1F), content = content) 70 | } 71 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/utils/DateUtils.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.utils 2 | 3 | import kotlinx.datetime.Clock 4 | import kotlinx.datetime.Instant 5 | import kotlinx.datetime.LocalDate 6 | import kotlinx.datetime.LocalDateTime 7 | import kotlinx.datetime.TimeZone 8 | import kotlinx.datetime.format.char 9 | import kotlinx.datetime.isoDayNumber 10 | import kotlinx.datetime.minus 11 | import kotlinx.datetime.toInstant 12 | import kotlinx.datetime.toLocalDateTime 13 | 14 | object DateUtils { 15 | 16 | val YMD_HMS = LocalDateTime.Format { 17 | year() 18 | char('-') 19 | monthNumber() 20 | char('-') 21 | dayOfMonth() 22 | char(' ') 23 | hour() 24 | char(':') 25 | minute() 26 | char(':') 27 | second() 28 | } 29 | 30 | val YMD_HM = LocalDateTime.Format { 31 | year() 32 | char('-') 33 | monthNumber() 34 | char('-') 35 | dayOfMonth() 36 | char(' ') 37 | hour() 38 | char(':') 39 | minute() 40 | } 41 | 42 | val YMD = LocalDate.Format { 43 | year() 44 | char('-') 45 | monthNumber() 46 | char('-') 47 | dayOfMonth() 48 | } 49 | 50 | val MD = LocalDate.Format { 51 | monthNumber() 52 | char('-') 53 | dayOfMonth() 54 | } 55 | 56 | val HM = LocalDateTime.Format { 57 | hour() 58 | char(':') 59 | minute() 60 | } 61 | 62 | fun today() = Clock.System.now().toLocalDateTime().date 63 | 64 | fun currentTime() = Clock.System.now().toLocalDateTime() 65 | 66 | /** 67 | * 计算时间差 68 | */ 69 | // fun timeDifference(s: String, e: String): String { 70 | // try { 71 | // val fromDate = YMD_HM.parse(s) 72 | // val toDate = YMD_HM.parse(e) 73 | // val from = fromDate.time 74 | // val to = toDate.time 75 | // val hours = ((to - from) / (1000 * 60 * 60)).toInt() 76 | // val minutes = ((to - from) / (1000 * 60)).toInt() % 60 77 | // return if(hours != 0) { 78 | // if(minutes == 0) { 79 | // hours.toString() + "小时" 80 | // } else { 81 | // hours.toString() + "小时" + minutes + "分钟" 82 | // } 83 | // } else { 84 | // minutes.toString() + "分钟" 85 | // } 86 | // } catch(ignored: Exception) { 87 | // return "" 88 | // } 89 | // } 90 | 91 | fun calcDayOffset(startTime: LocalDate, endTime: LocalDate): Int { 92 | val offset = endTime.minus(startTime) 93 | return offset.days + offset.months * 30 94 | } 95 | 96 | fun calcWeekOffset(startTime: LocalDate, endTime: LocalDate): Int { 97 | val dayOfWeek = startTime.dayOfWeek.isoDayNumber 98 | val dayOffset = calcDayOffset(startTime, endTime) 99 | return dayOffset / 7 + if(dayOffset > 0) { 100 | if((dayOffset % 7 + dayOfWeek) > 7) 1 else 0 101 | } else { 102 | if((dayOffset % 7 + dayOfWeek) < 1) -1 else 0 103 | } 104 | } 105 | } 106 | 107 | fun Instant.toLocalDateTime() = this.toLocalDateTime(TimeZone.UTC) 108 | 109 | fun LocalDateTime.toInstant() = this.toInstant(TimeZone.UTC) 110 | 111 | 112 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/drawables/InsertInvitation.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.drawables 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val Drawables.InsertInvitation: ImageVector 10 | get() { 11 | if (_InsertInvitation != null) { 12 | return _InsertInvitation!! 13 | } 14 | _InsertInvitation = ImageVector.Builder( 15 | name = "InsertInvitation", 16 | defaultWidth = 24.dp, 17 | defaultHeight = 24.dp, 18 | viewportWidth = 24f, 19 | viewportHeight = 24f 20 | ).apply { 21 | path(fill = SolidColor(Color(0xFF000000))) { 22 | moveTo(16f, 12f) 23 | horizontalLineToRelative(-3f) 24 | curveToRelative(-0.55f, 0f, -1f, 0.45f, -1f, 1f) 25 | verticalLineToRelative(3f) 26 | curveToRelative(0f, 0.55f, 0.45f, 1f, 1f, 1f) 27 | horizontalLineToRelative(3f) 28 | curveToRelative(0.55f, 0f, 1f, -0.45f, 1f, -1f) 29 | verticalLineToRelative(-3f) 30 | curveToRelative(0f, -0.55f, -0.45f, -1f, -1f, -1f) 31 | close() 32 | moveTo(16f, 2f) 33 | verticalLineToRelative(1f) 34 | lineTo(8f, 3f) 35 | lineTo(8f, 2f) 36 | curveToRelative(0f, -0.55f, -0.45f, -1f, -1f, -1f) 37 | reflectiveCurveToRelative(-1f, 0.45f, -1f, 1f) 38 | verticalLineToRelative(1f) 39 | lineTo(5f, 3f) 40 | curveToRelative(-1.11f, 0f, -1.99f, 0.9f, -1.99f, 2f) 41 | lineTo(3f, 19f) 42 | curveToRelative(0f, 1.1f, 0.89f, 2f, 2f, 2f) 43 | horizontalLineToRelative(14f) 44 | curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f) 45 | lineTo(21f, 5f) 46 | curveToRelative(0f, -1.1f, -0.9f, -2f, -2f, -2f) 47 | horizontalLineToRelative(-1f) 48 | lineTo(18f, 2f) 49 | curveToRelative(0f, -0.55f, -0.45f, -1f, -1f, -1f) 50 | reflectiveCurveToRelative(-1f, 0.45f, -1f, 1f) 51 | close() 52 | moveTo(18f, 19f) 53 | lineTo(6f, 19f) 54 | curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f) 55 | lineTo(5f, 8f) 56 | horizontalLineToRelative(14f) 57 | verticalLineToRelative(10f) 58 | curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f) 59 | close() 60 | } 61 | }.build() 62 | 63 | return _InsertInvitation!! 64 | } 65 | 66 | @Suppress("ObjectPropertyName") 67 | private var _InsertInvitation: ImageVector? = null 68 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/widget/form/Login.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.widget.form 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.text.KeyboardActions 6 | import androidx.compose.foundation.text.KeyboardOptions 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.IconButton 9 | import androidx.compose.material3.OutlinedTextField 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.MutableState 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.setValue 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.text.input.ImeAction 19 | import androidx.compose.ui.text.input.KeyboardType 20 | import androidx.compose.ui.text.input.PasswordVisualTransformation 21 | import androidx.compose.ui.text.input.VisualTransformation 22 | import androidx.compose.ui.unit.dp 23 | import com.qust.helper.ui.drawables.Drawables 24 | import com.qust.helper.ui.drawables.Visibility 25 | import com.qust.helper.ui.drawables.VisibilityOff 26 | 27 | 28 | @Composable 29 | fun AccountInput( 30 | account: MutableState, 31 | password: MutableState, 32 | accountError: String = "", 33 | passwordError: String = "", 34 | labelAccount: String = "账号", 35 | labelPassword: String = "密码", 36 | login: () -> Unit 37 | ){ 38 | var passwordHidden by remember{ mutableStateOf(true) } 39 | 40 | OutlinedTextField( 41 | value = account.value, 42 | onValueChange = { account.value = it }, 43 | modifier = Modifier.fillMaxWidth().padding(top = 8.dp), 44 | singleLine = true, 45 | label = { Text(text = labelAccount) }, 46 | isError = accountError.isNotEmpty(), 47 | supportingText = { if(accountError.isNotEmpty()) Text(text = accountError) }, 48 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next), 49 | maxLines = 1 50 | ) 51 | 52 | OutlinedTextField( 53 | value = password.value, 54 | onValueChange = { password.value = it }, 55 | modifier = Modifier.fillMaxWidth().padding(top = 8.dp), 56 | label = { Text(text = labelPassword) }, 57 | isError = passwordError.isNotEmpty(), 58 | supportingText = { if(passwordError.isNotEmpty()) Text(text = passwordError) }, 59 | trailingIcon = { 60 | IconButton(onClick = { passwordHidden = !passwordHidden }){ 61 | Icon(imageVector = (if(passwordHidden) Drawables.Visibility else Drawables.VisibilityOff), null) 62 | } 63 | }, 64 | visualTransformation = if(passwordHidden) PasswordVisualTransformation('*') else VisualTransformation.None, 65 | keyboardOptions = KeyboardOptions(autoCorrectEnabled = false, keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), 66 | keyboardActions = KeyboardActions(onDone = { login() }), 67 | maxLines = 1 68 | ) 69 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/drawables/Brightness.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.drawables 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val Drawables.Brightness: ImageVector 10 | get() { 11 | if (_Brightness != null) { 12 | return _Brightness!! 13 | } 14 | _Brightness = ImageVector.Builder( 15 | name = "Brightness", 16 | defaultWidth = 24.dp, 17 | defaultHeight = 24.dp, 18 | viewportWidth = 24f, 19 | viewportHeight = 24f 20 | ).apply { 21 | path(fill = SolidColor(Color(0xFF000000))) { 22 | moveTo(20f, 8.69f) 23 | lineTo(20f, 6f) 24 | curveToRelative(0f, -1.1f, -0.9f, -2f, -2f, -2f) 25 | horizontalLineToRelative(-2.69f) 26 | lineToRelative(-1.9f, -1.9f) 27 | curveToRelative(-0.78f, -0.78f, -2.05f, -0.78f, -2.83f, 0f) 28 | lineTo(8.69f, 4f) 29 | lineTo(6f, 4f) 30 | curveToRelative(-1.1f, 0f, -2f, 0.9f, -2f, 2f) 31 | verticalLineToRelative(2.69f) 32 | lineToRelative(-1.9f, 1.9f) 33 | curveToRelative(-0.78f, 0.78f, -0.78f, 2.05f, 0f, 2.83f) 34 | lineToRelative(1.9f, 1.9f) 35 | lineTo(4f, 18f) 36 | curveToRelative(0f, 1.1f, 0.9f, 2f, 2f, 2f) 37 | horizontalLineToRelative(2.69f) 38 | lineToRelative(1.9f, 1.9f) 39 | curveToRelative(0.78f, 0.78f, 2.05f, 0.78f, 2.83f, 0f) 40 | lineToRelative(1.9f, -1.9f) 41 | lineTo(18f, 20f) 42 | curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f) 43 | verticalLineToRelative(-2.69f) 44 | lineToRelative(1.9f, -1.9f) 45 | curveToRelative(0.78f, -0.78f, 0.78f, -2.05f, 0f, -2.83f) 46 | lineTo(20f, 8.69f) 47 | close() 48 | moveTo(12f, 18f) 49 | curveToRelative(-3.31f, 0f, -6f, -2.69f, -6f, -6f) 50 | reflectiveCurveToRelative(2.69f, -6f, 6f, -6f) 51 | reflectiveCurveToRelative(6f, 2.69f, 6f, 6f) 52 | reflectiveCurveToRelative(-2.69f, 6f, -6f, 6f) 53 | close() 54 | moveTo(12f, 8f) 55 | curveToRelative(-2.21f, 0f, -4f, 1.79f, -4f, 4f) 56 | reflectiveCurveToRelative(1.79f, 4f, 4f, 4f) 57 | reflectiveCurveToRelative(4f, -1.79f, 4f, -4f) 58 | reflectiveCurveToRelative(-1.79f, -4f, -4f, -4f) 59 | close() 60 | } 61 | }.build() 62 | 63 | return _Brightness!! 64 | } 65 | 66 | @Suppress("ObjectPropertyName") 67 | private var _Brightness: ImageVector? = null 68 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/drawables/VisibilityOff.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.drawables 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val Drawables.VisibilityOff: ImageVector 10 | get() { 11 | if (_VisibilityOff != null) { 12 | return _VisibilityOff!! 13 | } 14 | _VisibilityOff = ImageVector.Builder( 15 | name = "VisibilityOff", 16 | defaultWidth = 24.dp, 17 | defaultHeight = 24.dp, 18 | viewportWidth = 24f, 19 | viewportHeight = 24f 20 | ).apply { 21 | path(fill = SolidColor(Color(0xFF000000))) { 22 | moveTo(12f, 6.5f) 23 | curveToRelative(2.76f, 0f, 5f, 2.24f, 5f, 5f) 24 | curveToRelative(0f, 0.51f, -0.1f, 1f, -0.24f, 1.46f) 25 | lineToRelative(3.06f, 3.06f) 26 | curveToRelative(1.39f, -1.23f, 2.49f, -2.77f, 3.18f, -4.53f) 27 | curveTo(21.27f, 7.11f, 17f, 4f, 12f, 4f) 28 | curveToRelative(-1.27f, 0f, -2.49f, 0.2f, -3.64f, 0.57f) 29 | lineToRelative(2.17f, 2.17f) 30 | curveToRelative(0.47f, -0.14f, 0.96f, -0.24f, 1.47f, -0.24f) 31 | close() 32 | moveTo(2.71f, 3.16f) 33 | curveToRelative(-0.39f, 0.39f, -0.39f, 1.02f, 0f, 1.41f) 34 | lineToRelative(1.97f, 1.97f) 35 | curveTo(3.06f, 7.83f, 1.77f, 9.53f, 1f, 11.5f) 36 | curveTo(2.73f, 15.89f, 7f, 19f, 12f, 19f) 37 | curveToRelative(1.52f, 0f, 2.97f, -0.3f, 4.31f, -0.82f) 38 | lineToRelative(2.72f, 2.72f) 39 | curveToRelative(0.39f, 0.39f, 1.02f, 0.39f, 1.41f, 0f) 40 | curveToRelative(0.39f, -0.39f, 0.39f, -1.02f, 0f, -1.41f) 41 | lineTo(4.13f, 3.16f) 42 | curveToRelative(-0.39f, -0.39f, -1.03f, -0.39f, -1.42f, 0f) 43 | close() 44 | moveTo(12f, 16.5f) 45 | curveToRelative(-2.76f, 0f, -5f, -2.24f, -5f, -5f) 46 | curveToRelative(0f, -0.77f, 0.18f, -1.5f, 0.49f, -2.14f) 47 | lineToRelative(1.57f, 1.57f) 48 | curveToRelative(-0.03f, 0.18f, -0.06f, 0.37f, -0.06f, 0.57f) 49 | curveToRelative(0f, 1.66f, 1.34f, 3f, 3f, 3f) 50 | curveToRelative(0.2f, 0f, 0.38f, -0.03f, 0.57f, -0.07f) 51 | lineTo(14.14f, 16f) 52 | curveToRelative(-0.65f, 0.32f, -1.37f, 0.5f, -2.14f, 0.5f) 53 | close() 54 | moveTo(14.97f, 11.17f) 55 | curveToRelative(-0.15f, -1.4f, -1.25f, -2.49f, -2.64f, -2.64f) 56 | lineToRelative(2.64f, 2.64f) 57 | close() 58 | } 59 | }.build() 60 | 61 | return _VisibilityOff!! 62 | } 63 | 64 | @Suppress("ObjectPropertyName") 65 | private var _VisibilityOff: ImageVector? = null 66 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/data/QustApi.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.data 2 | 3 | object QustApi { 4 | /** 5 | * 智慧青科大VPN HOST 6 | */ 7 | const val VPN_HOST = "wvpn.qust.edu.cn" 8 | 9 | /** 10 | * 智慧青科大VPN登录入口 11 | * 12 | * Get: 13 | * - 登录界面 14 | * 15 | * Post: 16 | * - ul: 用户名长度 17 | * - pl: 密码长度 18 | * - lt: HTML里拿 19 | * - rsa: 加密后的用户名密码 20 | * - execution: e1s1 21 | * - _eventId: submit 22 | */ 23 | const val VPN_LOGIN = "https://wvpn.qust.edu.cn/" 24 | 25 | /** 26 | * 智慧青科大 HOST 27 | */ 28 | const val IPASS_HOST = "ipass.qust.edu.cn" 29 | 30 | /** 31 | * 智慧青科大登录入口 32 | * 33 | * Get: 34 | * - 登录界面 35 | * 36 | * Post: 37 | * - ul: 用户名长度 38 | * - pl: 密码长度 39 | * - lt: HTML里拿 40 | * - rsa: 加密后的用户名密码 41 | * - execution: e1s1 42 | * - _eventId: submit 43 | */ 44 | const val IPASS_LOGIN = "http://ipass.qust.edu.cn/tpass/login/" 45 | 46 | /** 47 | * 教务系统HOST 48 | */ 49 | val EA_HOSTS = arrayOf( 50 | "jwglxt.qust.edu.cn", 51 | "jwglxt1.qust.edu.cn", 52 | "jwglxt2.qust.edu.cn", 53 | "jwglxt3.qust.edu.cn", 54 | "jwglxt4.qust.edu.cn", 55 | "jwglxt5.qust.edu.cn", 56 | "jwglxt6.qust.edu.cn" 57 | ) 58 | 59 | 60 | /** 61 | * 教务登录 62 | * 63 | * Get: 64 | * - 登录界面 65 | * 66 | * Post: 67 | * - csrftoken: HTML里拿 68 | * - language: zh_CN 69 | * - yhm: 用户名 70 | * - mm: RSA加密后的密码 71 | */ 72 | const val EA_LOGIN = "jwglxt/xtgl/login_slogin.html" 73 | 74 | /** 75 | * 教务登录,获取RSA公钥 76 | */ 77 | const val EA_LOGIN_PUBLIC_KEY = "jwglxt/xtgl/login_getPublicKey.html" 78 | 79 | /** 80 | * 81 | */ 82 | const val EA_MAIN_MENU = "jwglxt/xtgl/index_initMenu.html" 83 | 84 | /** 85 | * 教务系统消息查询 86 | * 87 | * Post: 88 | * - queryModel.showCount: 一页显示几条 89 | * - queryModel.currentPage: 第几页 90 | * - queryModel.sortName: cjsj 91 | * - queryModel.sortOrder: desc 92 | */ 93 | const val EA_SYSTEM_NOTICE = "jwglxt/xtgl/index_cxDbsy.html?doType=query" 94 | 95 | /** 96 | * 学年信息 97 | */ 98 | const val EA_YEAR_DATA = "jwglxt/xtgl/index_cxAreaFive.html?localeKey=zh_CN&gnmkdm=index" 99 | 100 | /** 101 | * 查询学生课表 102 | */ 103 | const val GET_LESSON_TABLE = "jwglxt/kbcx/xskbcx_cxXsgrkb.html" 104 | 105 | /** 106 | * 查询班级课表 107 | */ 108 | const val GET_CLASS_LESSON_TABLE = "jwglxt/kbdy/bjkbdy_cxBjKb.html" 109 | 110 | /** 111 | * 推荐课表打印页面 112 | */ 113 | const val RECOMMENDED_LESSON_TABLE_PRINTING = "jwglxt/kbdy/bjkbdy_cxBjkbdyIndex.html?gnmkdm=0&layout=default" 114 | 115 | /** 116 | * 成绩查询 117 | */ 118 | const val GET_MARK = "jwglxt/cjcx/cjcx_cxXsgrcj.html?doType=query" 119 | 120 | /** 121 | * 成绩明细查询 122 | */ 123 | const val GET_MARK_DETAIL = "jwglxt/cjcx/cjcx_cxXsKccjList.html" 124 | 125 | /** 126 | * 考试查询 127 | */ 128 | const val GET_EXAM = "jwglxt/kwgl/kscx_cxXsksxxIndex.html?doType=query" 129 | 130 | /** 131 | * 学业情况查询界面 132 | */ 133 | const val ACADEMIC_PAGE = "jwglxt/xsxy/xsxyqk_cxXsxyqkIndex.html?gnmkdm=N105515&layout=default" 134 | 135 | /** 136 | * 学业情况查询 - 课程信息 137 | */ 138 | const val ACADEMIC_INFO = "jwglxt/xsxy/xsxyqk_cxJxzxjhxfyqKcxx.html" 139 | 140 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/drawables/GridView.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.drawables 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val Drawables.GridView: ImageVector 10 | get() { 11 | if (_GridView != null) { 12 | return _GridView!! 13 | } 14 | _GridView = ImageVector.Builder( 15 | name = "GridView", 16 | defaultWidth = 24.dp, 17 | defaultHeight = 24.dp, 18 | viewportWidth = 24f, 19 | viewportHeight = 24f 20 | ).apply { 21 | path(fill = SolidColor(Color.Black)) { 22 | moveTo(5f, 11f) 23 | horizontalLineToRelative(4f) 24 | curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f) 25 | verticalLineTo(5f) 26 | curveToRelative(0f, -1.1f, -0.9f, -2f, -2f, -2f) 27 | horizontalLineTo(5f) 28 | curveTo(3.9f, 3f, 3f, 3.9f, 3f, 5f) 29 | verticalLineToRelative(4f) 30 | curveTo(3f, 10.1f, 3.9f, 11f, 5f, 11f) 31 | close() 32 | } 33 | path(fill = SolidColor(Color.Black)) { 34 | moveTo(5f, 21f) 35 | horizontalLineToRelative(4f) 36 | curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f) 37 | verticalLineToRelative(-4f) 38 | curveToRelative(0f, -1.1f, -0.9f, -2f, -2f, -2f) 39 | horizontalLineTo(5f) 40 | curveToRelative(-1.1f, 0f, -2f, 0.9f, -2f, 2f) 41 | verticalLineToRelative(4f) 42 | curveTo(3f, 20.1f, 3.9f, 21f, 5f, 21f) 43 | close() 44 | } 45 | path(fill = SolidColor(Color.Black)) { 46 | moveTo(13f, 5f) 47 | verticalLineToRelative(4f) 48 | curveToRelative(0f, 1.1f, 0.9f, 2f, 2f, 2f) 49 | horizontalLineToRelative(4f) 50 | curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f) 51 | verticalLineTo(5f) 52 | curveToRelative(0f, -1.1f, -0.9f, -2f, -2f, -2f) 53 | horizontalLineToRelative(-4f) 54 | curveTo(13.9f, 3f, 13f, 3.9f, 13f, 5f) 55 | close() 56 | } 57 | path(fill = SolidColor(Color.Black)) { 58 | moveTo(15f, 21f) 59 | horizontalLineToRelative(4f) 60 | curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f) 61 | verticalLineToRelative(-4f) 62 | curveToRelative(0f, -1.1f, -0.9f, -2f, -2f, -2f) 63 | horizontalLineToRelative(-4f) 64 | curveToRelative(-1.1f, 0f, -2f, 0.9f, -2f, 2f) 65 | verticalLineToRelative(4f) 66 | curveTo(13f, 20.1f, 13.9f, 21f, 15f, 21f) 67 | close() 68 | } 69 | }.build() 70 | 71 | return _GridView!! 72 | } 73 | 74 | @Suppress("ObjectPropertyName") 75 | private var _GridView: ImageVector? = null 76 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/qust/helper/ui/page/eas/QueryNoticePage.kt: -------------------------------------------------------------------------------- 1 | package com.qust.helper.ui.page.eas 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.lazy.LazyColumn 9 | import androidx.compose.material.ExperimentalMaterialApi 10 | import androidx.compose.material.pullrefresh.PullRefreshIndicator 11 | import androidx.compose.material.pullrefresh.pullRefresh 12 | import androidx.compose.material.pullrefresh.rememberPullRefreshState 13 | import androidx.compose.material3.Card 14 | import androidx.compose.material3.CardDefaults 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.LaunchedEffect 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.text.style.TextAlign 22 | import androidx.compose.ui.unit.dp 23 | import androidx.lifecycle.viewmodel.compose.viewModel 24 | import com.qust.helper.entity.eas.Notice 25 | import com.qust.helper.ui.drawables.Drawables 26 | import com.qust.helper.ui.drawables.Notification 27 | import com.qust.helper.ui.page.BasePage 28 | import com.qust.helper.viewmodel.eas.QueryNoticeViewModel 29 | 30 | object QueryNoticePage: BasePage("教务通知", Drawables.Notification) { 31 | 32 | @Composable 33 | override fun getViewModel() = viewModel() 34 | 35 | @Composable 36 | @OptIn(ExperimentalMaterialApi::class) 37 | override fun Content(viewModel: QueryNoticeViewModel) { 38 | val state = rememberPullRefreshState(refreshing = viewModel.refreshing, onRefresh = { viewModel.queryNotice() }) 39 | LaunchedEffect(viewModel.hasRefresh){ 40 | if(viewModel.notices.isEmpty() && !viewModel.hasRefresh && !viewModel.refreshing){ viewModel.queryNotice() } 41 | } 42 | 43 | Box(modifier = Modifier.fillMaxSize().pullRefresh(state)){ 44 | LazyColumn(Modifier.fillMaxSize()){ 45 | items(viewModel.notices.size) { index -> 46 | NoticeItem(viewModel.notices[index]) 47 | } 48 | } 49 | PullRefreshIndicator(viewModel.refreshing, state, Modifier.align(Alignment.TopCenter)) 50 | } 51 | } 52 | 53 | @Composable 54 | fun NoticeItem(notice: Notice){ 55 | Card( 56 | modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), 57 | colors = CardDefaults.cardColors( 58 | containerColor = MaterialTheme.colorScheme.background, 59 | ), 60 | elevation = CardDefaults.cardElevation(defaultElevation = 6.dp), 61 | ) { 62 | 63 | Column(modifier = Modifier.fillMaxWidth()) { 64 | 65 | Text( 66 | modifier = Modifier.fillMaxWidth().padding(16.dp, 8.dp), 67 | text = notice.content, 68 | style = MaterialTheme.typography.bodyMedium, 69 | ) 70 | 71 | Text( 72 | text = notice.time, 73 | style = MaterialTheme.typography.bodySmall, 74 | textAlign = TextAlign.End, 75 | modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) 76 | ) 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /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 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | --------------------------------------------------------------------------------