diff --git a/.gitignore b/.gitignore index e4a4757..6df52ae 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ bin/ ### 默认在保存项目目录的课程(直接忽略中文字符命名文件) ### *\u{4e00}-\u{9fa5}* + +### 缓存数据库 ### +data.db \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 9e3aab3..e854a34 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,13 +8,16 @@ plugins { group = "rikacelery.github.io" //MAJOR.MINOR.BUILD //255.255.65535 -version = "1.0.21" +version = "1.0.3" repositories { google() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } +dependencies{ + +} kotlin { jvm { @@ -24,7 +27,6 @@ kotlin { sourceSets { val jvmMain by getting { - dependencies { implementation("org.jsoup:jsoup:1.16.1") val ktorVersion = "2.3.0" @@ -41,6 +43,11 @@ kotlin { implementation(compose.desktop.currentOs) implementation("org.jetbrains.kotlinx:kotlinx-coroutines:0.19.2") implementation("com.typesafe:config:1.4.1") + + implementation("org.jetbrains.exposed:exposed-core:0.36.2") + implementation("org.jetbrains.exposed:exposed-dao:0.36.2") + implementation("org.jetbrains.exposed:exposed-jdbc:0.36.2") + implementation("org.xerial:sqlite-jdbc:3.36.0.3") } } val jvmTest by getting @@ -51,6 +58,7 @@ compose.desktop { application { mainClass = "MainKt" nativeDistributions { + modules("java.sql")//fix ClassNotFound: java.sql.Driver windows { perUserInstall = true shortcut = true diff --git a/src/jvmMain/kotlin/Downloader.kt b/src/jvmMain/kotlin/Downloader.kt index acedcea..b223f3e 100644 --- a/src/jvmMain/kotlin/Downloader.kt +++ b/src/jvmMain/kotlin/Downloader.kt @@ -42,6 +42,7 @@ suspend fun downloadVideo(folder: File, teacherFile: File, pcFile: File, resourc if (States.tasks.get(resourceId + "_1")?.isActive == true) return@supervisorScope States.tasks[resourceId + "_1"] = launch { + States.progress[resourceId + "_1"] = 0f runCatching { downloadToFile(teacherFile, teacherUrl) { current, total, totalTime, _, startBytes -> if (total != -1L) { @@ -57,10 +58,10 @@ suspend fun downloadVideo(folder: File, teacherFile: File, pcFile: File, resourc States.progressInfo.remove(resourceId + "_1") }.onFailure { if (it is CancellationException) { - States.progressInfo.put(resourceId + "_1", "Cancelled") + States.progressInfo.put(resourceId + "_1", "暂停中") } else { - States.progressInfo.put(resourceId + "_1", "Failed") + States.progressInfo.put(resourceId + "_1", "下载失败") it.printStackTrace() } } @@ -69,6 +70,7 @@ suspend fun downloadVideo(folder: File, teacherFile: File, pcFile: File, resourc if (States.tasks.get(resourceId + "_2")?.isActive == true) return@supervisorScope States.tasks[resourceId + "_2"] = launch { + States.progress[resourceId + "_2"] = 0f runCatching { downloadToFile(pcFile, pcUrl) { current, total, totalTime, _, startBytes -> if (total != -1L) { @@ -84,14 +86,15 @@ suspend fun downloadVideo(folder: File, teacherFile: File, pcFile: File, resourc States.progressInfo.remove(resourceId + "_2") }.onFailure { if (it is CancellationException) { - States.progressInfo.put(resourceId + "_2", "Cancelled") + States.progressInfo.put(resourceId + "_2", "暂停中") } else { - States.progressInfo.put(resourceId + "_2", "Failed") + States.progressInfo.put(resourceId + "_2", "下载失败") it.printStackTrace() } } } + } } //播放器 diff --git a/src/jvmMain/kotlin/Main.kt b/src/jvmMain/kotlin/Main.kt index 2b477ec..cd063bb 100644 --- a/src/jvmMain/kotlin/Main.kt +++ b/src/jvmMain/kotlin/Main.kt @@ -1,3 +1,4 @@ + import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import io.ktor.client.* @@ -11,6 +12,7 @@ import kotlinx.serialization.json.Json import ui.MainPage import utils.OkHttpUtil import java.io.File +import java.io.PrintStream val client = HttpClient(OkHttp) { // install(Logging) { @@ -62,9 +64,16 @@ fun logOut() { } fun main() { - + val err = PrintStream(File("err.txt").outputStream()) + val out = PrintStream(File("out.txt").outputStream()) + System.setErr(err) + System.setErr(out) + States.loadAll() application { - Window(onCloseRequest = ::exitApplication) { + Window(onCloseRequest = { + States.saveAll() + exitApplication() + }) { MainPage() } } diff --git a/src/jvmMain/kotlin/States.kt b/src/jvmMain/kotlin/States.kt index 37f59cf..af99fa5 100644 --- a/src/jvmMain/kotlin/States.kt +++ b/src/jvmMain/kotlin/States.kt @@ -1,20 +1,29 @@ + import androidx.compose.runtime.* import kotlinx.coroutines.Job +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject +import utils.DB +import utils.conf.Conf +import utils.objectArray import java.io.File import java.util.concurrent.ConcurrentHashMap object States { - var conf=utils.conf.Conf var currentTerm: String = "" var videos = mutableStateListOf() var syncState: SyncState by mutableStateOf(SyncState.OUT_DATE) var pageState by mutableStateOf(PageState.INDEX) - var downloadFolder = File(conf.getConf("savepath")).canonicalFile - val progress = mutableStateMapOf() - val progressInfo = mutableStateMapOf() + val downloadFolder: File + get() = File(Conf.savePath).canonicalFile + + + val progress = mutableStateMapOf() + val progressInfo = mutableStateMapOf() - val tasks = ConcurrentHashMap() + val tasks = ConcurrentHashMap() var currentJob: Job? = null @@ -25,6 +34,28 @@ object States { var lessons = mutableStateListOf() var termNow by mutableStateOf("---") var terms = mutableStateListOf() - var cookie by mutableStateOf("") + var cookie: String by mutableStateOf("") + fun saveAll(){ + DB.setValue("currentTerm",currentTerm) + DB.setValue("videos_cache", Json.Default.encodeToString>(videos)) + DB.setValue("query_type", queryType.toString()) + DB.setValue("query_videos", Json.Default.encodeToString(queryVideos)) + DB.setValue("lesson_now", lessonNow) + DB.setValue("lessons", Json.Default.encodeToString>(lessons)) + DB.setValue("term_now", termNow) + DB.setValue("terms", Json.Default.encodeToString>(terms)) + DB.setValue("cookie_cache", cookie) + } + fun loadAll(){ + currentTerm = DB.getValue("currentTerm")?:"" + videos.addAll(DB.getValue("videos_cache")?.let { Json.parseToJsonElement(it).objectArray }?.toTypedArray()?: arrayOf()) + queryType = DB.getValue("query_type")?.toInt()?:-1 + queryVideos = DB.getValue("query_videos")?.let { Json.decodeFromString>>(it) }?: listOf() + lessonNow = DB.getValue("lesson_now")?:"---" + lessons.addAll(DB.getValue("lessons")?.let { Json.parseToJsonElement(it).objectArray }?.toTypedArray()?: arrayOf()) + termNow = DB.getValue("term_now")?:"---" + terms.addAll(DB.getValue("terms")?.let { Json.parseToJsonElement(it).objectArray }?.toTypedArray()?: arrayOf()) + cookie = DB.getValue("cookie_cache")?:"" + } } \ No newline at end of file diff --git a/src/jvmMain/kotlin/TopBar.kt b/src/jvmMain/kotlin/TopBar.kt index 5f9b91c..f044936 100644 --- a/src/jvmMain/kotlin/TopBar.kt +++ b/src/jvmMain/kotlin/TopBar.kt @@ -16,13 +16,9 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.TransformedText -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonArray import utils.String @Composable @@ -46,7 +42,10 @@ fun TopBar( ) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { TextField( - cookieString, setCookieString, Modifier.weight(1f, true).height(50.dp), visualTransformation = PasswordVisualTransformation() + cookieString, + setCookieString, + Modifier.weight(1f, true).height(50.dp), + visualTransformation = PasswordVisualTransformation() ) Spacer(Modifier.width(2.dp)) IconButton(onClick = onSync) { @@ -66,6 +65,7 @@ fun TopBar( SyncState.SYNCED -> { Icon(painterResource("done_outline.svg"), "refresh list") } + SyncState.SYNCING -> { Icon(painterResource("sync.svg"), "refresh list", Modifier.rotate(rotationState.value)) } @@ -85,7 +85,7 @@ fun TopBar( append(it.String("year")) append(it.String("name")) } - },setFilter2, Modifier) + }, setFilter2, Modifier) Spacer(Modifier.width(2.dp)) OutlinedButton( onFnClick, Modifier.size(50.dp), shape = CircleShape, contentPadding = PaddingValues(0.dp) diff --git a/src/jvmMain/kotlin/ui/MainPage.kt b/src/jvmMain/kotlin/ui/MainPage.kt index 779f177..d575a32 100644 --- a/src/jvmMain/kotlin/ui/MainPage.kt +++ b/src/jvmMain/kotlin/ui/MainPage.kt @@ -18,6 +18,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Settings import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -61,6 +62,7 @@ suspend fun updateVideoList() { }?.getOrNull()?.runCatching { States.videos.clear() States.videos.addAll(map { + buildJsonObject { put("lessonName", it.String("courseName")) put("date", it.String("scheduleTimeStart").substringBefore(' ')) @@ -96,6 +98,7 @@ suspend fun updateVideoList() { } } + //贝塞尔曲线 fun calculateY(t: Float): Float { val tSquared = t * t @@ -106,6 +109,7 @@ fun calculateY(t: Float): Float { return y.toFloat() } + suspend fun updateTermVideoList(termId: String) { States.syncState = SyncState.SYNCING require(States.queryVideos != null) @@ -253,7 +257,6 @@ fun MainPage() { PageState.SETTINGS -> "Back" }, onFnClick = { - logOut(States.pageState) when (States.pageState) { PageState.INDEX -> States.pageState = PageState.SETTINGS PageState.SETTINGS -> States.pageState = PageState.INDEX @@ -262,6 +265,7 @@ fun MainPage() { syncState = States.syncState, onSync = { if (States.syncState != SyncState.SYNCING) { + DB.setValue("cookie_cache", States.cookie) syncCourses(mainScope) } }) @@ -378,25 +382,24 @@ fun MainPage() { } } Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceBetween) { - Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter ) { States.progress[id + "_1"]?.let { - LinearProgressIndicator( it, - Modifier.height(8.dp).weight(1f), + Modifier.height(8.dp).fillMaxWidth(1f), color = Color.hsv(calculateY(it) * 120, 1f, 1f) ) - } ?: Spacer(Modifier.height(8.dp).weight(1f)) + } ?: Spacer(Modifier.height(8.dp).fillMaxWidth(1f)) Text(States.progressInfo[id + "_1"] ?: "") } - Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.Bottom) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.BottomCenter) { States.progress[id + "_2"]?.let { LinearProgressIndicator( it, - Modifier.height(8.dp).weight(1f), + Modifier.height(8.dp).fillMaxWidth(1f), color = Color.hsv(calculateY(it) * 120, 1f, 1f) ) - } ?: Spacer(Modifier.height(8.dp).weight(1f)) + } ?: Spacer(Modifier.height(8.dp).fillMaxWidth(1f)) Text(States.progressInfo[id + "_2"] ?: "") } } @@ -416,6 +419,11 @@ fun MainPage() { } } + LaunchedEffect(Unit){ + if (States.cookie.isNotEmpty()&&States.videos.isEmpty()){ + syncCourses(mainScope) + } + } } } diff --git a/src/jvmMain/kotlin/ui/SettingPage.kt b/src/jvmMain/kotlin/ui/SettingPage.kt index 15e2d83..414749f 100644 --- a/src/jvmMain/kotlin/ui/SettingPage.kt +++ b/src/jvmMain/kotlin/ui/SettingPage.kt @@ -1,4 +1,3 @@ - import androidx.compose.foundation.layout.* import androidx.compose.material.Button import androidx.compose.material.OutlinedTextField @@ -14,7 +13,7 @@ import javax.swing.filechooser.FileSystemView @Composable fun SettingPage() { - var savePath by remember { mutableStateOf("") } // use a mutable state variable + var savePath by remember { mutableStateOf(Conf.savePath) } // use a mutable state variable Row( modifier = Modifier.fillMaxWidth().padding(16.dp), @@ -23,40 +22,38 @@ fun SettingPage() { OutlinedTextField( value = savePath, onValueChange = { savePath = it }, // update the mutable state variable - label = { Text(text = "保存路径") }, + label = { Text(text = savePath) }, modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.width(8.dp)) - Button(onClick = { savePath = chooseDirectory() }) { // update the mutable state variable - Text(text = "选择路径") + Button(onClick = { + val directory = chooseDirectory() + if (directory != null) + savePath = directory + }) { // update the mutable state variable + Text(text = "保存路径") } Spacer(modifier = Modifier.width(8.dp)) - Button(onClick = { saveToFile(savePath) }) { + Button(onClick = { Conf.savePath = savePath }) { Text(text = "保存") } } } -private fun saveToFile(savePath: String) { - val conf =utils.conf.Conf - if (savePath.isNotEmpty()) { - conf.setConf("savepath",savePath) - } -} -private fun chooseDirectory(): String { - val chooser = JFileChooser() - chooser.currentDirectory = File(Conf.getConf("savepath")).let { if (it.exists()) it else File(".") } +private fun chooseDirectory(): String? { + val chooser = JFileChooser(FileSystemView.getFileSystemView()) + chooser.currentDirectory = File(Conf.savePath).let { if (it.exists()) it else File(".") } chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY val result = chooser.showOpenDialog(null) return if (result == JFileChooser.APPROVE_OPTION) { chooser.selectedFile.absolutePath } else { - "" + null } } \ No newline at end of file diff --git a/src/jvmMain/kotlin/utils/DB.kt b/src/jvmMain/kotlin/utils/DB.kt new file mode 100644 index 0000000..df9fbb5 --- /dev/null +++ b/src/jvmMain/kotlin/utils/DB.kt @@ -0,0 +1,58 @@ +package utils + +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction + +object DB { + object ConfTable : Table("configs") { + val key = text("key").uniqueIndex() + val value = text("value") + override val primaryKey: PrimaryKey? + get() = PrimaryKey(key) + } + + fun getValue(key:String):String?{ + return transaction{ + ConfTable.select { + ConfTable.key eq key + }.singleOrNull()?.get(ConfTable.value) + } + } + fun setValue(key:String,value:String):String?{ + return transaction { + val oldValue = ConfTable.select { + ConfTable.key eq key + }.singleOrNull()?.get(ConfTable.value) + if (oldValue==null) { + // 插入默认值 + ConfTable.insert { + it[ConfTable.key] = key + it[ConfTable.value] = value + } + } else { + // 更新现有值 + ConfTable.update({ConfTable.key eq key}) { + it[ConfTable.value] = value + } + } + oldValue + } + } + + // 设置数据库连接 + private fun setupDatabase() { + Database.connect("jdbc:sqlite:data.db", "org.sqlite.JDBC") + } + + // 创建表 + private fun createTable() { + transaction { + SchemaUtils.create(ConfTable) + } + } + + init { + setupDatabase() + createTable() + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/utils/JsonExtensions.kt b/src/jvmMain/kotlin/utils/JsonExtensions.kt index 7c3c56a..9f6e6bc 100644 --- a/src/jvmMain/kotlin/utils/JsonExtensions.kt +++ b/src/jvmMain/kotlin/utils/JsonExtensions.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.json.* class JsonElementCastException(msg: String) : Exception(msg) -@Suppress("FunctionName","unused") +@Suppress("FunctionName", "unused") fun JsonElement.Object(key: String): JsonObject { val obj = this as? JsonObject ?: throw JsonElementCastException("Cannot cast to JsonObject") if (!obj.containsKey(key)) { @@ -17,7 +17,7 @@ fun JsonElement.Object(key: String): JsonObject { return value } -@Suppress("FunctionName","unused") +@Suppress("FunctionName", "unused") fun JsonElement.Array(key: String): JsonArray { val obj = this as? JsonObject ?: throw JsonElementCastException("Cannot cast to JsonObject") if (!obj.containsKey(key)) { @@ -29,7 +29,8 @@ fun JsonElement.Array(key: String): JsonArray { } return value } -@Suppress("FunctionName","unused") + +@Suppress("FunctionName", "unused") fun JsonElement.ObjectArray(key: String): List { val obj = this as? JsonObject ?: throw JsonElementCastException("Cannot cast to JsonObject") if (!obj.containsKey(key)) { @@ -45,6 +46,19 @@ fun JsonElement.ObjectArray(key: String): List { return value.map { it as JsonObject } } +@Suppress("unused") +val JsonElement.objectArray: List + get() { + val value = this + if (value !is JsonArray) { + throw JsonElementCastException("This is not a JsonArray, this '$value'") + } + if (!value.all { it is JsonObject }) + throw JsonElementCastException("Values of this array are not all JsonObject, this '$value'") + + return value.map { it as JsonObject } + } + @Suppress("unused") fun JsonElement.String(key: String): String { val obj = this as? JsonObject ?: throw JsonElementCastException("Cannot cast to JsonObject") diff --git a/src/jvmMain/kotlin/utils/conf/Conf.kt b/src/jvmMain/kotlin/utils/conf/Conf.kt index 0da969e..5febcec 100644 --- a/src/jvmMain/kotlin/utils/conf/Conf.kt +++ b/src/jvmMain/kotlin/utils/conf/Conf.kt @@ -1,32 +1,30 @@ package utils.conf -import logOut -import java.io.File +import utils.DB +import kotlin.io.path.Path +import kotlin.io.path.absolutePathString object Conf { - private val home = System.getProperty("user.home") - private val configFile = File(home, ".downloader.conf") - private val config: MutableMap = mutableMapOf() - - init { - if (configFile.exists()) { - configFile.readLines().forEach { - val (key, value) = it.split("=") - config[key] = value + private var savePathCache: String? = null + var savePath: String + get() { + val path = if (savePathCache != null) { + savePathCache + } else { + savePathCache = DB.getValue("save_path") + println("cache path $savePathCache") + savePathCache + } + return path ?: Path(".").absolutePathString() + } + set(value) { + if (value != savePathCache) { + savePathCache = value + DB.setValue("save_path", value) + println("update savePathCache to $value") } - }else{ - configFile.createNewFile() - setConf("savepath",".") + println("set save path to $value") } - } - fun setConf(key: String, value: String) { - config[key] = value - configFile.writeText(config.map { "${it.key}=${it.value}" }.joinToString("\n")) - logOut("$key set to $value") - } - fun getConf(key: String): String? { - return config[key] - } } \ No newline at end of file