diff --git a/app/src/main/java/com/fduhole/danxinative/base/feature/FudanDiningHallCrowdednessFeature.kt b/app/src/main/java/com/fduhole/danxinative/base/feature/FudanDiningHallCrowdednessFeature.kt new file mode 100644 index 0000000..78bf4c1 --- /dev/null +++ b/app/src/main/java/com/fduhole/danxinative/base/feature/FudanDiningHallCrowdednessFeature.kt @@ -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? = 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 -> {} + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fduhole/danxinative/model/DiningInfo.kt b/app/src/main/java/com/fduhole/danxinative/model/DiningInfo.kt new file mode 100644 index 0000000..58b8d6b --- /dev/null +++ b/app/src/main/java/com/fduhole/danxinative/model/DiningInfo.kt @@ -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) diff --git a/app/src/main/java/com/fduhole/danxinative/repository/fdu/DatacenterRepository.kt b/app/src/main/java/com/fduhole/danxinative/repository/fdu/DatacenterRepository.kt new file mode 100644 index 0000000..0b91f9f --- /dev/null +++ b/app/src/main/java/com/fduhole/danxinative/repository/fdu/DatacenterRepository.kt @@ -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("") + + val chartData = data.substring(begin, end).replace("'", "\"") + // val chartData = DATA.replace("'", "\"") + + val jsonExtraction = "\\[.+\\]".toRegex().findAll(chartData) + val names = Json.decodeFromString>( + jsonExtraction.elementAt(areaCode * 3).groups[0]!!.value) + val currentData = Json.decodeFromString>( + jsonExtraction.elementAt(areaCode * 3 + 1).groups[0]!!.value) + val highestData = Json.decodeFromString>( + 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"; +} \ No newline at end of file diff --git a/app/src/main/java/com/fduhole/danxinative/state/GlobalState.kt b/app/src/main/java/com/fduhole/danxinative/state/GlobalState.kt index 1cc745a..093fc60 100644 --- a/app/src/main/java/com/fduhole/danxinative/state/GlobalState.kt +++ b/app/src/main/java/com/fduhole/danxinative/state/GlobalState.kt @@ -20,6 +20,7 @@ val appModule = module { single { FDUHoleRepository() } single { AAORepository() } single { LibraryRepository() } + single { DatacenterRepository() } } class GlobalState constructor(private val sp: SharedPreferences) { diff --git a/app/src/main/java/com/fduhole/danxinative/ui/HomeViewModel.kt b/app/src/main/java/com/fduhole/danxinative/ui/HomeViewModel.kt index 3049956..bad82c3 100644 --- a/app/src/main/java/com/fduhole/danxinative/ui/HomeViewModel.kt +++ b/app/src/main/java/com/fduhole/danxinative/ui/HomeViewModel.kt @@ -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 @@ -18,7 +19,8 @@ class HomeViewModel : ViewModel() { _uiState.emit(HomeUiState(listOf( FudanDailyFeature(), FudanAAONoticesFeature(), - FudanLibraryAttendanceFeature() + FudanLibraryAttendanceFeature(), + FudanDiningHallCrowdednessFeature() ))) } diff --git a/app/src/main/java/com/fduhole/danxinative/util/DataUtils.kt b/app/src/main/java/com/fduhole/danxinative/util/DataUtils.kt new file mode 100644 index 0000000..580a9ae --- /dev/null +++ b/app/src/main/java/com/fduhole/danxinative/util/DataUtils.kt @@ -0,0 +1,16 @@ +package com.fduhole.danxinative.util + +object DataUtils { + fun zipToTriple(first: List, second: List, third: List) + : List> { + 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>(minSize) + for (i in 0 until minSize) { + list.add(Triple(first[i], second[i], third[i])) + } + return list + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fduhole/danxinative/util/ErrorUtils.kt b/app/src/main/java/com/fduhole/danxinative/util/ErrorUtils.kt index 945f851..b167ada 100644 --- a/app/src/main/java/com/fduhole/danxinative/util/ErrorUtils.kt +++ b/app/src/main/java/com/fduhole/danxinative/util/ErrorUtils.kt @@ -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)