Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dining hall crowdedness feature [WIP] #12

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.fduhole.danxinative.base.feature

import com.fduhole.danxinative.base.Feature
import com.fduhole.danxinative.R
import com.fduhole.danxinative.model.DiningInfoItem
import com.fduhole.danxinative.repository.fdu.DatacenterRepository
import com.fduhole.danxinative.util.UnsuitableTimeException
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.*

class FudanDiningHallCrowdednessFeature : Feature(), KoinComponent {
enum class Status {
IDLE, LOADING, LOADED, UNSUITABLE, FAILED, ERROR
}

private val repo: DatacenterRepository by inject()

private var subTitleContent = ""
private var status: Status = Status.IDLE
private var loadingJob: Job? = null
private var trafficInfo: List<DiningInfoItem>? = null
private var mostCrowded: DiningInfoItem? = null
private var leastCrowded: DiningInfoItem? = null

override fun getClickable(): Boolean = true
override fun getIconId(): Int = R.drawable.ic_baseline_forum_24
override fun getTitle(): String = "食堂排队消费状况"
override fun getSubTitle(): String = when (status) {
Status.IDLE -> "轻触以查看"
Status.FAILED -> "加载失败"
Status.ERROR -> "发生错误,轻触重试"
Status.LOADING -> "加载中"
Status.UNSUITABLE -> "现在不是用餐时间"
Status.LOADED -> subTitleContent
}

override fun onClick() {
loadingJob?.cancel()
when (status) {
Status.IDLE,
Status.FAILED,
Status.ERROR,
Status.UNSUITABLE -> {
loadingJob = featureScope.launch {
try {
trafficInfo = trafficInfo ?: repo.getCrowdednessInfo(0)
status = Status.LOADED
}
catch (e: UnsuitableTimeException) { status = Status.UNSUITABLE }
catch (e: Throwable) { status = Status.ERROR }
notifyRefresh()
}
}
else -> {}
}
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/com/fduhole/danxinative/model/DiningInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.fduhole.danxinative.model

import kotlinx.serialization.Serializable

@Serializable
data class DiningInfoItem(val name: String, val current: Int, val highest: Int)
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.fduhole.danxinative.repository.fdu

import com.fduhole.danxinative.model.DiningInfoItem
import com.fduhole.danxinative.util.DataUtils
import com.fduhole.danxinative.util.UnsuitableTimeException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Request
import kotlin.coroutines.resume

class DatacenterRepository : BaseFDURepository() {
companion object {
const val DINING_DETAIL_URL = "https://my.fudan.edu.cn/simple_list/stqk"
const val DINING_HALL_CLOSING_NOTICE = "本功能仅在用餐时段开放"
const val DATA = "initChart('chart_bb', ['光华楼\\n光华咖啡','光华楼-光华咖啡(学府餐饮)','光华楼-光华咖啡(学校餐饮)','北区食堂-北区二楼德保','北区\\n千喜鹤','北区\\n新世纪早餐','北区食堂-北区新世纪早餐(高校)','北区\\n清真','北区食堂-北区清真(伊源)','北区\\n西餐厅','北区食堂-北区西餐厅(乐烹西东)','北区\\n面包房','北区食堂-北区面包房(东兴鼎昊)','北区\\n颐谷','南区\\n一楼同茂兴','南区\\n中快餐饮','南区\\n清真','南区食堂-南区清真(伊源)','南区\\n南苑餐厅','南区食堂-南苑餐厅(东大)','南区\\n同茂兴','南区\\n教工快餐','南区食堂-教工快餐(东大)','文图咖啡馆','旦苑\\n清真','旦苑\\n一楼大厅','旦苑\\n教授餐厅','旦苑\\n二楼大厅','旦苑\\n面包房','旦苑-本部学校面包房(学校餐饮)','旦苑-本部西餐厅(乐烹西东)','旦苑\\n西餐厅'],\n['0','0','0','0','0','0','9','0','0','0','2','0','3','1','3','0','0','0','0','0','0','0','0','0','0','7','0','0','0','2','0','0'],\n['5','2','2','77','118','79','66','45','36','54','30','52','21','134','167','51','49','36','100','74','113','171','109','10','79','232','44','167','75','57','35','46'])\ninitChart('chart_fl', ['书院楼西园餐厅','书院楼西园餐厅(养吉)','书院楼风味餐厅','书院楼风味餐厅(颐谷)','护理学院','枫林清真餐厅-枫林清真餐厅','枫林清真餐厅-枫林清真餐厅(伊源)','枫林食堂-枫林一楼科桥'],\n['0','0','0','5','0','0','0','1'],\n['49','26','167','107','37','60','49','144'])\ninitChart('chart_jw', ['一楼中快','二楼颐谷','清真','清真(伊源)','点心','点心(中快)','花园餐厅','花园餐厅(雷汇柏祺)'],\n['13','0','0','1','0','1','0','0'],\n['209','133','46','40','39','29','10','1'])\ninitChart('chart_zj', ['一餐二楼教师','一餐二楼自选','一餐二楼风味','一楼中快','佳乐餐饮','清真','清真(伊源)'],\n['0','0','0','0','0','0','0'],\n['22','41','23','67','29','32','26'])\n"
}

suspend fun getCrowdednessInfo(areaCode: Int) = withContext(Dispatchers.IO) {
suspendCancellableCoroutine {
val response = client.newCall(
Request.Builder()
.url(DINING_DETAIL_URL).get()
.build()
).execute()
val data = response.body!!.string()
if (data.contains(DINING_HALL_CLOSING_NOTICE)) {
throw UnsuitableTimeException()
}

val begin = data.indexOf("initChart('")
val end = data.lastIndexOf("</script>")

val chartData = data.substring(begin, end).replace("'", "\"")
// val chartData = DATA.replace("'", "\"")

val jsonExtraction = "\\[.+\\]".toRegex().findAll(chartData)
val names = Json.decodeFromString<List<String>>(
jsonExtraction.elementAt(areaCode * 3).groups[0]!!.value)
val currentData = Json.decodeFromString<List<Int>>(
jsonExtraction.elementAt(areaCode * 3 + 1).groups[0]!!.value)
val highestData = Json.decodeFromString<List<Int>>(
jsonExtraction.elementAt(areaCode * 3 + 2).groups[0]!!.value)

val datas = DataUtils.zipToTriple(names, currentData, highestData)
it.resume(datas.map { it -> DiningInfoItem(it.first, it.second, it.third) })
}
}

override fun getHost(): String = "my.fudan.edu.cn"

override fun getUISLoginURL(): String =
"https://uis.fudan.edu.cn/authserver/login?service=https%3A%2F%2Fmy.fudan.edu.cn%2Fsimple_list%2Fstqk";
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ val appModule = module {
single { FDUHoleRepository() }
single { AAORepository() }
single { LibraryRepository() }
single { DatacenterRepository() }
}

class GlobalState constructor(private val sp: SharedPreferences) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fduhole.danxinative.base.feature.FudanAAONoticesFeature
import com.fduhole.danxinative.base.feature.FudanDailyFeature
import com.fduhole.danxinative.base.feature.FudanDiningHallCrowdednessFeature
import com.fduhole.danxinative.base.feature.FudanLibraryAttendanceFeature
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -18,7 +19,8 @@ class HomeViewModel : ViewModel() {
_uiState.emit(HomeUiState(listOf(
FudanDailyFeature(),
FudanAAONoticesFeature(),
FudanLibraryAttendanceFeature()
FudanLibraryAttendanceFeature(),
FudanDiningHallCrowdednessFeature()
)))
}

Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/com/fduhole/danxinative/util/DataUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.fduhole.danxinative.util

object DataUtils {
fun <T, U, V> zipToTriple(first: List<T>, second: List<U>, third: List<V>)
: List<Triple<T, U, V>> {
val minSize =
if (first.size <= second.size && first.size <= third.size) first.size
else if (second.size <= first.size && second.size <= third.size) second.size
else third.size
val list = ArrayList<Triple<T, U, V>>(minSize)
for (i in 0 until minSize) {
list.add(Triple(first[i], second[i], third[i]))
}
return list
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/com/fduhole/danxinative/util/ErrorUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ abstract class ExplainableException : Exception() {
abstract fun explain(context: Resources): String
}

/**
* UnsuitableTimeException is related to Dining Crowdedness.
*/
class UnsuitableTimeException : Exception() {
}

class ErrorUtils {
companion object {
fun describeError(context: Context, error: Throwable): String = describeError(context.resources, error)
Expand Down